Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions runtime/ai/developer_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ func (t *DeveloperAgent) Handler(ctx context.Context, args *DeveloperAgentArgs)
return nil, err
}
if args.CurrentFilePath != "" {
_, err := s.CallTool(ctx, RoleAssistant, ReadFileName, nil, &ReadFileArgs{
_, _ = s.CallTool(ctx, RoleAssistant, ReadFileName, nil, &ReadFileArgs{
Path: args.CurrentFilePath,
})
if err != nil {
return nil, err
if ctx.Err() != nil { // Ignore tool error since the file may not exist
return nil, ctx.Err()
}
}

Expand Down
2 changes: 1 addition & 1 deletion runtime/ai/instructions/data/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ The following tools are typically available for project development:
- `project_status` for checking resource names and their current status (idle, running, error)
- `query_sql` for running SQL against a connector; use `SELECT` statements with `LIMIT` clauses and low timeouts, and be mindful of performance or making too many queries
- `query_metrics_view` for querying a metrics view; useful for answering data questions and validating dashboard behavior
- `list_tables` and `get_table` for accessing the information schema of a database connector
- `list_tables` and `show_table` for accessing the information schema of a database connector
- `list_buckets` and `list_bucket_files` for exploring files in object stores like S3 or GCS; to preview file contents, load one file into a table using a model and query it with `query_sql`

{% if .external %}
Expand Down
17 changes: 11 additions & 6 deletions runtime/ai/show_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,29 @@ var _ Tool[*ShowTableArgs, *ShowTableResult] = (*ShowTable)(nil)

type ShowTableArgs struct {
Connector string `json:"connector,omitempty" jsonschema:"Optional OLAP connector name. Defaults to the instance's default OLAP connector."`
Table string `json:"table" jsonschema:"The name of the table to describe."`
Database string `json:"database,omitempty" jsonschema:"Optional database name for connectors that support multiple databases."`
DatabaseSchema string `json:"database_schema,omitempty" jsonschema:"Optional database schema name."`
Table string `json:"table" jsonschema:"Name of the table to describe. Must be a simple table name; database/schema names should be provided using the separate fields."`
Database string `json:"database,omitempty" jsonschema:"Database that contains the table (defaults to the connector's default database if applicable)."`
DatabaseSchema string `json:"database_schema,omitempty" jsonschema:"Database schema that contains the table (defaults to the connector's default schema if applicable)."`
}

type ShowTableResult struct {
Name string `json:"name"`
IsView bool `json:"is_view"`
Columns []ColumnInfo `json:"columns"`
PhysicalSizeBytes int64 `json:"physical_size_bytes,omitempty" jsonschema:"The physical size of the table in bytes. If 0 or omitted, size information is not available."`
DDL string `json:"ddl,omitempty" jsonschema:"The SQL DDL statement (CREATE TABLE/VIEW) for this table, if available."`
}

type ColumnInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Name string `json:"name" jsonschema:"The name of the column."`
Type string `json:"type" jsonschema:"The data type of the column. This is a generic type code and does not exactly match the underlying SQL type."`
}

func (t *ShowTable) Spec() *mcp.Tool {
return &mcp.Tool{
Name: ShowTableName,
Title: "Show Table",
Description: "Show schema and column information for a table in an OLAP connector.",
Description: "Show schema and column information for a table in an OLAP connector. Note: Table, schema and database names passed to this tool are case sensitive; if you get an error and you're working with a database that folds unquoted identifiers (e.g Snowflake folds to uppercase), you may need to retry with the casing adjusted accordingly.",
Meta: map[string]any{
"openai/toolInvocation/invoking": "Getting table schema...",
"openai/toolInvocation/invoked": "Got table schema",
Expand Down Expand Up @@ -75,11 +76,15 @@ func (t *ShowTable) Handler(ctx context.Context, args *ShowTableArgs) (*ShowTabl
// Load physical size
_ = olap.InformationSchema().LoadPhysicalSize(ctx, []*drivers.OlapTable{table})

// Load DDL
_ = olap.InformationSchema().LoadDDL(ctx, table)

// Build result
result := &ShowTableResult{
Name: table.Name,
IsView: table.View,
PhysicalSizeBytes: table.PhysicalSizeBytes,
DDL: table.DDL,
Columns: make([]ColumnInfo, 0),
}

Expand Down
5 changes: 5 additions & 0 deletions runtime/drivers/athena/olap.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ func (c *Connection) LoadPhysicalSize(ctx context.Context, tables []*drivers.Ola
return nil
}

// LoadDDL implements drivers.OLAPInformationSchema.
func (c *Connection) LoadDDL(ctx context.Context, table *drivers.OlapTable) error {
return nil // Not implemented
}

// Lookup implements drivers.OLAPInformationSchema.
func (c *Connection) Lookup(ctx context.Context, db, schema, name string) (*drivers.OlapTable, error) {
meta, err := c.GetTable(ctx, db, schema, name)
Expand Down
29 changes: 29 additions & 0 deletions runtime/drivers/bigquery/olap.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,35 @@ func (c *Connection) LoadPhysicalSize(ctx context.Context, tables []*drivers.Ola
return nil
}

// LoadDDL implements drivers.OLAPInformationSchema.
func (c *Connection) LoadDDL(ctx context.Context, table *drivers.OlapTable) error {
client, err := c.getClient(ctx)
if err != nil {
return err
}

q := fmt.Sprintf("SELECT ddl FROM `%s.%s.INFORMATION_SCHEMA.TABLES` WHERE table_name = @name", table.Database, table.DatabaseSchema)
cq := client.Query(q)
cq.Parameters = []bigquery.QueryParameter{
{Name: "name", Value: table.Name},
}

it, err := cq.Read(ctx)
if err != nil {
return err
}

var row struct {
DDL string `bigquery:"ddl"`
}
err = it.Next(&row)
if err != nil {
return err
}
table.DDL = row.DDL
return nil
}

// Lookup implements drivers.OLAPInformationSchema.
func (c *Connection) Lookup(ctx context.Context, db, schema, name string) (*drivers.OlapTable, error) {
client, err := c.getClient(ctx)
Expand Down
12 changes: 12 additions & 0 deletions runtime/drivers/bigquery/olap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,18 @@ func TestExec(t *testing.T) {
require.NoError(t, err)
}

func TestLoadDDL(t *testing.T) {
testmode.Expensive(t)
_, olap := acquireTestBigQuery(t)

table, err := olap.InformationSchema().Lookup(t.Context(), "rilldata", "integration_test", "all_datatypes")
require.NoError(t, err)
err = olap.InformationSchema().LoadDDL(t.Context(), table)
require.NoError(t, err)
require.NotEmpty(t, table.DDL)
require.Contains(t, table.DDL, "all_datatypes")
}

func TestScan(t *testing.T) {
testmode.Expensive(t)
_, olap := acquireTestBigQuery(t)
Expand Down
21 changes: 21 additions & 0 deletions runtime/drivers/clickhouse/information_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,27 @@ func (c *Connection) LoadPhysicalSize(ctx context.Context, tables []*drivers.Ola
return err
}

func (c *Connection) LoadDDL(ctx context.Context, table *drivers.OlapTable) error {
conn, release, err := c.acquireMetaConn(ctx)
if err != nil {
return err
}
defer func() { _ = release() }()

schema := table.DatabaseSchema
if schema == "" {
schema = c.config.Database // In Clickhouse, this is actually like a schema
}

var ddl string
err = conn.QueryRowxContext(ctx, fmt.Sprintf("SHOW CREATE TABLE %s.%s", safeSQLName(schema), safeSQLName(table.Name))).Scan(&ddl)
if err != nil {
return err
}
table.DDL = ddl
return nil
}

func scanTables(rows *sqlx.Rows) ([]*drivers.OlapTable, error) {
var res []*drivers.OlapTable

Expand Down
22 changes: 22 additions & 0 deletions runtime/drivers/clickhouse/information_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestInformationSchema(t *testing.T) {
t.Run("testInformationSchemaGetTable", func(t *testing.T) { testInformationSchemaGetTable(t, infoSchema) })
t.Run("testInformationSchemaListDatabaseSchemasPagination", func(t *testing.T) { testInformationSchemaListDatabaseSchemasPagination(t, infoSchema) })
t.Run("testInformationSchemaListTablesPagination", func(t *testing.T) { testInformationSchemaListTablesPagination(t, infoSchema) })
t.Run("testLoadDDL", func(t *testing.T) { testLoadDDL(t, conn) })
}

func testInformationSchemaAll(t *testing.T, conn drivers.Handle) {
Expand Down Expand Up @@ -374,3 +375,24 @@ func prepareConn(t *testing.T, conn drivers.Handle) {
})
require.NoError(t, err)
}

func testLoadDDL(t *testing.T, conn drivers.Handle) {
olap, _ := conn.AsOLAP("")
ctx := context.Background()

// Test DDL for a table
table, err := olap.InformationSchema().Lookup(ctx, "", "", "foo")
require.NoError(t, err)
err = olap.InformationSchema().LoadDDL(ctx, table)
require.NoError(t, err)
require.Contains(t, table.DDL, "CREATE TABLE")
require.Contains(t, table.DDL, "foo")

// Test DDL for a view
view, err := olap.InformationSchema().Lookup(ctx, "", "", "model")
require.NoError(t, err)
err = olap.InformationSchema().LoadDDL(ctx, view)
require.NoError(t, err)
require.Contains(t, view.DDL, "CREATE VIEW")
require.Contains(t, view.DDL, "model")
}
4 changes: 4 additions & 0 deletions runtime/drivers/druid/information_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ func (c *connection) Lookup(ctx context.Context, db, schema, name string) (*driv
return tables[0], nil
}

func (c *connection) LoadDDL(ctx context.Context, table *drivers.OlapTable) error {
return nil // Not implemented
}

func (c *connection) LoadPhysicalSize(ctx context.Context, tables []*drivers.OlapTable) error {
q := `SELECT
datasource,
Expand Down
15 changes: 15 additions & 0 deletions runtime/drivers/duckdb/information_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,21 @@ func (c *connection) LoadPhysicalSize(ctx context.Context, tables []*drivers.Ola
return nil
}

func (c *connection) LoadDDL(ctx context.Context, table *drivers.OlapTable) error {
db, release, err := c.acquireDB()
if err != nil {
return err
}
defer func() { _ = release() }()

ddl, err := db.DDL(ctx, table.Database, table.DatabaseSchema, table.Name)
if err != nil {
return c.checkErr(err)
}
table.DDL = ddl
return nil
}

func scanTables(rows []*rduckdb.Table) ([]*drivers.OlapTable, error) {
var res []*drivers.OlapTable

Expand Down
21 changes: 21 additions & 0 deletions runtime/drivers/duckdb/information_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func TestInformationSchema(t *testing.T) {
t.Run("testInformationSchemaListTables", func(t *testing.T) { testInformationSchemaListTables(t, infoSchema, database, databaseSchema) })
t.Run("testInformationSchemaGetTable", func(t *testing.T) { testInformationSchemaGetTable(t, infoSchema, database, databaseSchema) })
t.Run("testInformationSchemaListTablesPagination", func(t *testing.T) { testInformationSchemaListTablesPagination(t, infoSchema, database, databaseSchema) })
t.Run("testLoadDDL", func(t *testing.T) { testLoadDDL(t, olap) })
}

func TestInformationSchemaMotherduck(t *testing.T) {
Expand Down Expand Up @@ -283,3 +284,23 @@ func testInformationSchemaListTablesPagination(t *testing.T, infoSchema drivers.
require.Equal(t, 6, len(tables))
require.Empty(t, nextToken)
}

func testLoadDDL(t *testing.T, olap drivers.OLAPStore) {
ctx := context.Background()

// Test DDL for a materialized table
table, err := olap.InformationSchema().Lookup(ctx, "", "", "bar")
require.NoError(t, err)
err = olap.InformationSchema().LoadDDL(ctx, table)
require.NoError(t, err)
require.Contains(t, table.DDL, "CREATE TABLE")
require.Contains(t, table.DDL, "bar")

// Test DDL for a view
view, err := olap.InformationSchema().Lookup(ctx, "", "", "model")
require.NoError(t, err)
err = olap.InformationSchema().LoadDDL(ctx, view)
require.NoError(t, err)
require.Contains(t, view.DDL, "CREATE VIEW")
require.Contains(t, view.DDL, "model")
}
34 changes: 34 additions & 0 deletions runtime/drivers/mysql/olap.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,40 @@ func (c *connection) LoadPhysicalSize(ctx context.Context, tables []*drivers.Ola
return nil
}

// LoadDDL implements drivers.OLAPInformationSchema.
func (c *connection) LoadDDL(ctx context.Context, table *drivers.OlapTable) error {
db, err := c.getDB(ctx)
if err != nil {
return err
}

// SHOW CREATE TABLE works for both tables and views in MySQL.
// For tables it returns columns: [Table, Create Table].
// For views it returns columns: [View, Create View, character_set_client, collation_connection].
// We extract the DDL by column name to avoid depending on column order or count.
rows, err := db.QueryxContext(ctx, fmt.Sprintf("SHOW CREATE TABLE %s", drivers.DialectMySQL.EscapeTable(table.Database, table.DatabaseSchema, table.Name)))
if err != nil {
return err
}
defer rows.Close()

if rows.Next() {
res := make(map[string]any)
if err := rows.MapScan(res); err != nil {
return err
}
for _, key := range []string{"Create Table", "Create View"} {
if v, ok := res[key]; ok && v != nil {
if b, ok := v.([]byte); ok {
table.DDL = string(b)
}
break
}
}
}
return rows.Err()
}

// Lookup implements drivers.OLAPInformationSchema.
func (c *connection) Lookup(ctx context.Context, db, schema, name string) (*drivers.OlapTable, error) {
meta, err := c.GetTable(ctx, db, schema, name)
Expand Down
31 changes: 30 additions & 1 deletion runtime/drivers/mysql/olap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ func TestOLAP(t *testing.T) {
t.Run("Test Scan Full Table", func(t *testing.T) {
testFullTableScan(t, olap)
})

t.Run("Test LoadDDL", func(t *testing.T) {
testLoadDDL(t, olap)
})
}

func testMapScan(t *testing.T, olap drivers.OLAPStore) {
Expand Down Expand Up @@ -486,6 +488,33 @@ func testFullTableScan(t *testing.T, olap drivers.OLAPStore) {
require.Equal(t, count, 3)
}

func testLoadDDL(t *testing.T, olap drivers.OLAPStore) {
// Test DDL for a table
table, err := olap.InformationSchema().Lookup(t.Context(), "", "", "all_datatypes")
require.NoError(t, err)
err = olap.InformationSchema().LoadDDL(t.Context(), table)
require.NoError(t, err)
require.Contains(t, table.DDL, "CREATE TABLE")
require.Contains(t, table.DDL, "all_datatypes")

// Create a view and test DDL for it
err = olap.Exec(t.Context(), &drivers.Statement{Query: "CREATE OR REPLACE VIEW test_ddl_view AS SELECT int_col, varchar_col FROM all_datatypes"})
require.NoError(t, err)
t.Cleanup(func() {
_ = olap.Exec(t.Context(), &drivers.Statement{Query: "DROP VIEW IF EXISTS test_ddl_view"})
})

view, err := olap.InformationSchema().Lookup(t.Context(), "", "", "test_ddl_view")
require.NoError(t, err)
err = olap.InformationSchema().LoadDDL(t.Context(), view)
require.NoError(t, err)
// MySQL's DDL output for views is wack, so splitting the pieces out like this. Not worth fixing as it does contain the essential information.
require.Contains(t, strings.ToLower(view.DDL), "create")
require.Contains(t, strings.ToLower(view.DDL), "view")
require.Contains(t, strings.ToLower(view.DDL), "test_ddl_view")
require.Contains(t, strings.ToLower(view.DDL), "as select")
}

func acquireTestMySQL(t *testing.T) (drivers.Handle, drivers.OLAPStore) {
cfg := testruntime.AcquireConnector(t, "mysql")
conn, err := drivers.Open("mysql", "default", cfg, storage.MustNew(t.TempDir(), nil), activity.NewNoopClient(), zap.NewNop())
Expand Down
4 changes: 4 additions & 0 deletions runtime/drivers/olap.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ type OLAPInformationSchema interface {
// LoadPhysicalSize populates the PhysicalSizeBytes field of table metadata.
// It should be called after All or Lookup and not on manually created tables.
LoadPhysicalSize(ctx context.Context, tables []*OlapTable) error
// LoadDDL populates the DDL field of a single table's metadata.
// Drivers that don't support DDL retrieval should return nil (leaving DDL empty).
LoadDDL(ctx context.Context, table *OlapTable) error
}

// OlapTable represents a table in an information schema.
Expand All @@ -190,6 +193,7 @@ type OlapTable struct {
Schema *runtimev1.StructType
UnsupportedCols map[string]string
PhysicalSizeBytes int64
DDL string
}

// Dialect enumerates OLAP query languages.
Expand Down
5 changes: 5 additions & 0 deletions runtime/drivers/pinot/information_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,11 @@ func (c *connection) Lookup(ctx context.Context, db, schema, name string) (*driv
return table, nil
}

// LoadDDL implements drivers.OLAPInformationSchema.
func (c *connection) LoadDDL(ctx context.Context, table *drivers.OlapTable) error {
return nil // Not implemented
}

// LoadPhysicalSize populates the PhysicalSizeBytes field of the tables.
// This was not tested when implemented so should be tested when pinot becomes a fairly used connector.
func (c *connection) LoadPhysicalSize(ctx context.Context, tables []*drivers.OlapTable) error {
Expand Down
Loading
Loading