Skip to content

Commit 34b0213

Browse files
authored
fix: resolve compatibilities for Metabase (#340) (#352)
* fix: solve issue of handling extended query (#342) * fix: add internal view pg_stat_user_tables and add query rewritten for type casting * create internal macros to mimic some pg system functions * wip: add pg_catalog.pg_get_indexdef * fix: use a better regex pattern to replace the sys function names * fix: make 'SET SESSION CHARACTERISTICS TRANSACTION ...' work and use session level pgtypes.Map for each session to encode results * fix: cast DuckDB HUGEINT to pgtype.Numeric * fix: adopt CR feedbacks * fix: add tests and resolve failed tests
1 parent 4145be1 commit 34b0213

File tree

13 files changed

+935
-161
lines changed

13 files changed

+935
-161
lines changed

catalog/internal_macro.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package catalog
2+
3+
import "strings"
4+
5+
type MacroDefinition struct {
6+
Params []string
7+
DDL string
8+
}
9+
10+
type InternalMacro struct {
11+
Schema string
12+
Name string
13+
IsTableMacro bool
14+
// A macro can be overloaded with multiple definitions, each with a different set of parameters.
15+
// https://duckdb.org/docs/sql/statements/create_macro.html#overloading
16+
Definitions []MacroDefinition
17+
}
18+
19+
func (v *InternalMacro) QualifiedName() string {
20+
if strings.ToLower(v.Schema) == "pg_catalog" {
21+
return "__sys__." + v.Name
22+
}
23+
return v.Schema + "." + v.Name
24+
}
25+
26+
var InternalMacros = []InternalMacro{
27+
{
28+
Schema: "information_schema",
29+
Name: "_pg_expandarray",
30+
IsTableMacro: true,
31+
Definitions: []MacroDefinition{
32+
{
33+
Params: []string{"a"},
34+
DDL: `SELECT STRUCT_PACK(
35+
x := unnest(a),
36+
n := generate_series(1, array_length(a))
37+
) AS item`,
38+
},
39+
},
40+
},
41+
{
42+
Schema: "pg_catalog",
43+
Name: "pg_get_indexdef",
44+
IsTableMacro: false,
45+
Definitions: []MacroDefinition{
46+
{
47+
Params: []string{"index_oid"},
48+
// Do nothing currently
49+
DDL: `''`,
50+
},
51+
{
52+
Params: []string{"index_oid", "column_no", "pretty_bool"},
53+
// Do nothing currently
54+
DDL: `''`,
55+
},
56+
},
57+
},
58+
}

catalog/internal_tables.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ var InternalTables = struct {
168168
PGProc InternalTable
169169
PGClass InternalTable
170170
PGNamespace InternalTable
171+
PGMatViews InternalTable
171172
}{
172173
PersistentVariable: InternalTable{
173174
Schema: "__sys__",
@@ -608,6 +609,52 @@ var InternalTables = struct {
608609
"nspacl TEXT",
609610
InitialData: InitialDataTables.PGNamespace,
610611
},
612+
// View "pg_catalog.pg_matviews"
613+
// postgres=# \d+ pg_catalog.pg_matviews
614+
// View "pg_catalog.pg_matviews"
615+
// Column | Type | Collation | Nullable | Default | Storage | Description
616+
//--------------+---------+-----------+----------+---------+----------+-------------
617+
// schemaname | name | | | | plain |
618+
// matviewname | name | | | | plain |
619+
// matviewowner | name | | | | plain |
620+
// tablespace | name | | | | plain |
621+
// hasindexes | boolean | | | | plain |
622+
// ispopulated | boolean | | | | plain |
623+
// definition | text | | | | extended |
624+
//View definition:
625+
// SELECT n.nspname AS schemaname,
626+
// c.relname AS matviewname,
627+
// pg_get_userbyid(c.relowner) AS matviewowner,
628+
// t.spcname AS tablespace,
629+
// c.relhasindex AS hasindexes,
630+
// c.relispopulated AS ispopulated,
631+
// pg_get_viewdef(c.oid) AS definition
632+
// FROM pg_class c
633+
// LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
634+
// LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
635+
// WHERE c.relkind = 'm'::"char";
636+
PGMatViews: InternalTable{
637+
Schema: "__sys__",
638+
Name: "pg_matviews",
639+
KeyColumns: []string{
640+
"schemaname",
641+
"matviewname",
642+
},
643+
ValueColumns: []string{
644+
"matviewowner",
645+
"tablespace",
646+
"hasindexes",
647+
"ispopulated",
648+
"definition",
649+
},
650+
DDL: "schemaname VARCHAR NOT NULL, " +
651+
"matviewname VARCHAR NOT NULL, " +
652+
"matviewowner VARCHAR, " +
653+
"tablespace VARCHAR, " +
654+
"hasindexes BOOLEAN, " +
655+
"ispopulated BOOLEAN, " +
656+
"definition TEXT",
657+
},
611658
}
612659

613660
var internalTables = []InternalTable{
@@ -621,6 +668,7 @@ var internalTables = []InternalTable{
621668
InternalTables.PGProc,
622669
InternalTables.PGClass,
623670
InternalTables.PGNamespace,
671+
InternalTables.PGMatViews,
624672
}
625673

626674
func GetInternalTables() []InternalTable {

catalog/internal_views.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package catalog
2+
3+
type InternalView struct {
4+
Schema string
5+
Name string
6+
DDL string
7+
}
8+
9+
func (v *InternalView) QualifiedName() string {
10+
return v.Schema + "." + v.Name
11+
}
12+
13+
var InternalViews = []InternalView{
14+
{
15+
Schema: "__sys__",
16+
Name: "pg_stat_user_tables",
17+
DDL: `SELECT
18+
t.table_schema || '.' || t.table_name AS relid, -- Create a unique ID for the table
19+
t.table_schema AS schemaname, -- Schema name
20+
t.table_name AS relname, -- Table name
21+
0 AS seq_scan, -- Default to 0 (DuckDB doesn't track this)
22+
NULL AS last_seq_scan, -- Placeholder (DuckDB doesn't track this)
23+
0 AS seq_tup_read, -- Default to 0
24+
0 AS idx_scan, -- Default to 0
25+
NULL AS last_idx_scan, -- Placeholder
26+
0 AS idx_tup_fetch, -- Default to 0
27+
0 AS n_tup_ins, -- Default to 0 (inserted tuples not tracked)
28+
0 AS n_tup_upd, -- Default to 0 (updated tuples not tracked)
29+
0 AS n_tup_del, -- Default to 0 (deleted tuples not tracked)
30+
0 AS n_tup_hot_upd, -- Default to 0 (HOT updates not tracked)
31+
0 AS n_tup_newpage_upd, -- Default to 0 (new page updates not tracked)
32+
0 AS n_live_tup, -- Default to 0 (live tuples not tracked)
33+
0 AS n_dead_tup, -- Default to 0 (dead tuples not tracked)
34+
0 AS n_mod_since_analyze, -- Default to 0
35+
0 AS n_ins_since_vacuum, -- Default to 0
36+
NULL AS last_vacuum, -- Placeholder
37+
NULL AS last_autovacuum, -- Placeholder
38+
NULL AS last_analyze, -- Placeholder
39+
NULL AS last_autoanalyze, -- Placeholder
40+
0 AS vacuum_count, -- Default to 0
41+
0 AS autovacuum_count, -- Default to 0
42+
0 AS analyze_count, -- Default to 0
43+
0 AS autoanalyze_count -- Default to 0
44+
FROM
45+
information_schema.tables t
46+
WHERE
47+
t.table_type = 'BASE TABLE'; -- Include only base tables (not views)`,
48+
},
49+
{
50+
Schema: "__sys__",
51+
Name: "pg_index",
52+
DDL: `SELECT
53+
ROW_NUMBER() OVER () AS indexrelid, -- Simulated unique ID for the index
54+
t.table_oid AS indrelid, -- OID of the table
55+
COUNT(k.column_name) AS indnatts, -- Number of columns included in the index
56+
COUNT(k.column_name) AS indnkeyatts, -- Number of key columns in the index (same as indnatts here)
57+
CASE
58+
WHEN c.constraint_type = 'UNIQUE' THEN TRUE
59+
ELSE FALSE
60+
END AS indisunique, -- Indicates if the index is unique
61+
CASE
62+
WHEN c.constraint_type = 'PRIMARY KEY' THEN TRUE
63+
ELSE FALSE
64+
END AS indisprimary, -- Indicates if the index is a primary key
65+
ARRAY_AGG(k.ordinal_position ORDER BY k.ordinal_position) AS indkey, -- Array of column positions
66+
ARRAY[]::BIGINT[] AS indcollation, -- DuckDB does not support collation, set to default
67+
ARRAY[]::BIGINT[] AS indclass, -- DuckDB does not support index class, set to default
68+
ARRAY[]::INTEGER[] AS indoption, -- DuckDB does not support index options, set to default
69+
NULL AS indexprs, -- DuckDB does not support expression indexes, set to NULL
70+
NULL AS indpred -- DuckDB does not support partial indexes, set to NULL
71+
FROM
72+
information_schema.key_column_usage k
73+
JOIN
74+
information_schema.table_constraints c
75+
ON k.constraint_name = c.constraint_name
76+
AND k.table_name = c.table_name
77+
JOIN
78+
duckdb_tables() t
79+
ON k.table_name = t.table_name
80+
AND k.table_schema = t.schema_name
81+
WHERE
82+
c.constraint_type IN ('PRIMARY KEY', 'UNIQUE') -- Only select primary key and unique constraints
83+
GROUP BY
84+
t.table_oid, c.constraint_type, c.constraint_name
85+
ORDER BY
86+
t.table_oid;`,
87+
},
88+
}

catalog/provider.go

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ func NewDBProvider(defaultTimeZone, dataDir, defaultDB string) (prov *DatabasePr
5757
dataDir: dataDir,
5858
}
5959

60-
shouldInit := true
6160
if defaultDB == "" || defaultDB == "memory" {
6261
prov.defaultCatalogName = "memory"
6362
prov.dbFile = ""
@@ -66,8 +65,6 @@ func NewDBProvider(defaultTimeZone, dataDir, defaultDB string) (prov *DatabasePr
6665
prov.defaultCatalogName = defaultDB
6766
prov.dbFile = defaultDB + ".db"
6867
prov.dsn = filepath.Join(prov.dataDir, prov.dbFile)
69-
_, err = os.Stat(prov.dsn)
70-
shouldInit = os.IsNotExist(err)
7168
}
7269

7370
prov.connector, err = duckdb.NewConnector(prov.dsn, nil)
@@ -94,11 +91,9 @@ func NewDBProvider(defaultTimeZone, dataDir, defaultDB string) (prov *DatabasePr
9491
}
9592
}
9693

97-
if shouldInit {
98-
err = prov.initCatalog()
99-
if err != nil {
100-
return nil, err
101-
}
94+
err = prov.initCatalog()
95+
if err != nil {
96+
return nil, err
10297
}
10398

10499
err = prov.attachCatalogs()
@@ -182,6 +177,47 @@ func (prov *DatabaseProvider) initCatalog() error {
182177
}
183178
}
184179

180+
for _, v := range InternalViews {
181+
if _, err := prov.storage.ExecContext(
182+
context.Background(),
183+
"CREATE SCHEMA IF NOT EXISTS "+v.Schema,
184+
); err != nil {
185+
return fmt.Errorf("failed to create internal schema %q: %w", v.Schema, err)
186+
}
187+
if _, err := prov.storage.ExecContext(
188+
context.Background(),
189+
"CREATE VIEW IF NOT EXISTS "+v.QualifiedName()+" AS "+v.DDL,
190+
); err != nil {
191+
return fmt.Errorf("failed to create internal view %q: %w", v.Name, err)
192+
}
193+
}
194+
195+
for _, m := range InternalMacros {
196+
if _, err := prov.storage.ExecContext(
197+
context.Background(),
198+
"CREATE SCHEMA IF NOT EXISTS "+m.Schema,
199+
); err != nil {
200+
return fmt.Errorf("failed to create internal schema %q: %w", m.Schema, err)
201+
}
202+
definitions := make([]string, 0, len(m.Definitions))
203+
for _, d := range m.Definitions {
204+
macroParams := strings.Join(d.Params, ", ")
205+
var asType string
206+
if m.IsTableMacro {
207+
asType = "TABLE\n"
208+
} else {
209+
asType = "\n"
210+
}
211+
definitions = append(definitions, fmt.Sprintf("\n(%s) AS %s%s", macroParams, asType, d.DDL))
212+
}
213+
if _, err := prov.storage.ExecContext(
214+
context.Background(),
215+
"CREATE OR REPLACE MACRO "+m.QualifiedName()+strings.Join(definitions, ",")+";",
216+
); err != nil {
217+
return fmt.Errorf("failed to create internal macro %q: %w", m.Name, err)
218+
}
219+
}
220+
185221
if _, err := prov.pool.ExecContext(context.Background(), "PRAGMA enable_checkpoint_on_shutdown"); err != nil {
186222
logrus.WithError(err).Fatalln("Failed to enable checkpoint on shutdown")
187223
}

pgserver/connection_data.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,26 @@ type ConvertedStatement struct {
5858
Tag string
5959
PgParsable bool
6060
HasSentRowDesc bool
61+
IsExtendedQuery bool
6162
SubscriptionConfig *SubscriptionConfig
6263
BackupConfig *BackupConfig
6364
RestoreConfig *RestoreConfig
6465
}
6566

67+
func (cs ConvertedStatement) WithQueryString(queryString string) ConvertedStatement {
68+
return ConvertedStatement{
69+
String: queryString,
70+
AST: cs.AST,
71+
Tag: cs.Tag,
72+
PgParsable: cs.PgParsable,
73+
HasSentRowDesc: cs.HasSentRowDesc,
74+
IsExtendedQuery: cs.IsExtendedQuery,
75+
SubscriptionConfig: cs.SubscriptionConfig,
76+
BackupConfig: cs.BackupConfig,
77+
RestoreConfig: cs.RestoreConfig,
78+
}
79+
}
80+
6681
// copyFromStdinState tracks the metadata for an import of data into a table using a COPY FROM STDIN statement. When
6782
// this statement is processed, the server accepts COPY DATA messages from the client with chunks of data to load
6883
// into a table.

0 commit comments

Comments
 (0)