|
| 1 | +package server |
| 2 | + |
| 3 | +import ( |
| 4 | + "database/sql" |
| 5 | + "regexp" |
| 6 | + "strings" |
| 7 | +) |
| 8 | + |
| 9 | +// initPgCatalog creates PostgreSQL compatibility functions and views in DuckDB |
| 10 | +// DuckDB already has a pg_catalog schema with basic views, so we just add missing functions |
| 11 | +func initPgCatalog(db *sql.DB) error { |
| 12 | + // Create our own pg_database view that has all the columns psql expects |
| 13 | + // We put it in main schema and rewrite queries to use it |
| 14 | + pgDatabaseSQL := ` |
| 15 | + CREATE OR REPLACE VIEW pg_database AS |
| 16 | + SELECT |
| 17 | + 1::INTEGER AS oid, |
| 18 | + current_database() AS datname, |
| 19 | + 0::INTEGER AS datdba, |
| 20 | + 6::INTEGER AS encoding, |
| 21 | + 'en_US.UTF-8' AS datcollate, |
| 22 | + 'en_US.UTF-8' AS datctype, |
| 23 | + false AS datistemplate, |
| 24 | + true AS datallowconn, |
| 25 | + -1::INTEGER AS datconnlimit, |
| 26 | + NULL AS datacl |
| 27 | + ` |
| 28 | + db.Exec(pgDatabaseSQL) |
| 29 | + |
| 30 | + // Create helper macros/functions that psql expects but DuckDB doesn't have |
| 31 | + // These need to be created without schema prefix so DuckDB finds them |
| 32 | + functions := []string{ |
| 33 | + // pg_get_userbyid - returns username for a role OID |
| 34 | + `CREATE OR REPLACE MACRO pg_get_userbyid(id) AS 'duckdb'`, |
| 35 | + // pg_table_is_visible - checks if table is in search path |
| 36 | + `CREATE OR REPLACE MACRO pg_table_is_visible(oid) AS true`, |
| 37 | + // has_schema_privilege - check schema access |
| 38 | + `CREATE OR REPLACE MACRO has_schema_privilege(schema, priv) AS true`, |
| 39 | + `CREATE OR REPLACE MACRO has_schema_privilege(u, schema, priv) AS true`, |
| 40 | + // has_table_privilege - check table access |
| 41 | + `CREATE OR REPLACE MACRO has_table_privilege(table_name, priv) AS true`, |
| 42 | + `CREATE OR REPLACE MACRO has_table_privilege(u, table_name, priv) AS true`, |
| 43 | + // pg_encoding_to_char - convert encoding ID to name |
| 44 | + `CREATE OR REPLACE MACRO pg_encoding_to_char(enc) AS 'UTF8'`, |
| 45 | + // format_type - format a type OID as string |
| 46 | + `CREATE OR REPLACE MACRO format_type(type_oid, typemod) AS |
| 47 | + CASE type_oid |
| 48 | + WHEN 16 THEN 'boolean' |
| 49 | + WHEN 17 THEN 'bytea' |
| 50 | + WHEN 20 THEN 'bigint' |
| 51 | + WHEN 21 THEN 'smallint' |
| 52 | + WHEN 23 THEN 'integer' |
| 53 | + WHEN 25 THEN 'text' |
| 54 | + WHEN 700 THEN 'real' |
| 55 | + WHEN 701 THEN 'double precision' |
| 56 | + WHEN 1042 THEN 'character' |
| 57 | + WHEN 1043 THEN 'character varying' |
| 58 | + WHEN 1082 THEN 'date' |
| 59 | + WHEN 1083 THEN 'time' |
| 60 | + WHEN 1114 THEN 'timestamp' |
| 61 | + WHEN 1184 THEN 'timestamp with time zone' |
| 62 | + WHEN 1700 THEN 'numeric' |
| 63 | + WHEN 2950 THEN 'uuid' |
| 64 | + ELSE 'unknown' |
| 65 | + END`, |
| 66 | + // obj_description - get object comment |
| 67 | + `CREATE OR REPLACE MACRO obj_description(oid, catalog) AS NULL`, |
| 68 | + // col_description - get column comment |
| 69 | + `CREATE OR REPLACE MACRO col_description(table_oid, col_num) AS NULL`, |
| 70 | + // shobj_description - get shared object comment |
| 71 | + `CREATE OR REPLACE MACRO shobj_description(oid, catalog) AS NULL`, |
| 72 | + // pg_get_indexdef - get index definition |
| 73 | + `CREATE OR REPLACE MACRO pg_get_indexdef(index_oid) AS ''`, |
| 74 | + `CREATE OR REPLACE MACRO pg_get_indexdef(index_oid, col, pretty) AS ''`, |
| 75 | + // pg_get_constraintdef - get constraint definition |
| 76 | + `CREATE OR REPLACE MACRO pg_get_constraintdef(constraint_oid) AS ''`, |
| 77 | + `CREATE OR REPLACE MACRO pg_get_constraintdef(constraint_oid, pretty) AS ''`, |
| 78 | + // current_setting - get config setting |
| 79 | + `CREATE OR REPLACE MACRO current_setting(name) AS |
| 80 | + CASE name |
| 81 | + WHEN 'server_version' THEN '15.0' |
| 82 | + WHEN 'server_encoding' THEN 'UTF8' |
| 83 | + ELSE '' |
| 84 | + END`, |
| 85 | + // pg_is_in_recovery - check if in recovery mode |
| 86 | + `CREATE OR REPLACE MACRO pg_is_in_recovery() AS false`, |
| 87 | + } |
| 88 | + |
| 89 | + for _, f := range functions { |
| 90 | + if _, err := db.Exec(f); err != nil { |
| 91 | + // Log but don't fail - some might already exist or conflict |
| 92 | + continue |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + return nil |
| 97 | +} |
| 98 | + |
| 99 | +// pgCatalogFunctions is the list of functions we provide that psql calls with pg_catalog. prefix |
| 100 | +var pgCatalogFunctions = []string{ |
| 101 | + "pg_get_userbyid", |
| 102 | + "pg_table_is_visible", |
| 103 | + "pg_get_expr", |
| 104 | + "pg_encoding_to_char", |
| 105 | + "format_type", |
| 106 | + "obj_description", |
| 107 | + "col_description", |
| 108 | + "shobj_description", |
| 109 | + "pg_get_indexdef", |
| 110 | + "pg_get_constraintdef", |
| 111 | + "current_setting", |
| 112 | + "pg_is_in_recovery", |
| 113 | + "has_schema_privilege", |
| 114 | + "has_table_privilege", |
| 115 | + "array_to_string", |
| 116 | +} |
| 117 | + |
| 118 | +// pgCatalogFuncRegex matches pg_catalog.function_name( patterns |
| 119 | +var pgCatalogFuncRegex *regexp.Regexp |
| 120 | + |
| 121 | +func init() { |
| 122 | + // Build regex to match pg_catalog.func_name patterns |
| 123 | + funcPattern := strings.Join(pgCatalogFunctions, "|") |
| 124 | + pgCatalogFuncRegex = regexp.MustCompile(`(?i)pg_catalog\.(` + funcPattern + `)\s*\(`) |
| 125 | +} |
| 126 | + |
| 127 | +// Regex patterns for query rewriting |
| 128 | +var ( |
| 129 | + // OPERATOR(pg_catalog.~) -> ~ |
| 130 | + operatorRegex = regexp.MustCompile(`(?i)OPERATOR\s*\(\s*pg_catalog\.([~!<>=]+)\s*\)`) |
| 131 | + // COLLATE pg_catalog.default -> (remove) |
| 132 | + collateRegex = regexp.MustCompile(`(?i)\s+COLLATE\s+pg_catalog\."?default"?`) |
| 133 | + // pg_catalog.pg_database -> pg_database (use our view) |
| 134 | + pgDatabaseRegex = regexp.MustCompile(`(?i)pg_catalog\.pg_database`) |
| 135 | + // ::pg_catalog.regtype::pg_catalog.text -> ::VARCHAR (PostgreSQL type cast) |
| 136 | + regtypeTextCastRegex = regexp.MustCompile(`(?i)::pg_catalog\.regtype::pg_catalog\.text`) |
| 137 | + // ::pg_catalog.regtype -> ::VARCHAR |
| 138 | + regtypeCastRegex = regexp.MustCompile(`(?i)::pg_catalog\.regtype`) |
| 139 | + // ::pg_catalog.text -> ::VARCHAR |
| 140 | + textCastRegex = regexp.MustCompile(`(?i)::pg_catalog\.text`) |
| 141 | +) |
| 142 | + |
| 143 | +// rewritePgCatalogQuery rewrites PostgreSQL-specific syntax for DuckDB compatibility |
| 144 | +func rewritePgCatalogQuery(query string) string { |
| 145 | + // Replace pg_catalog.func_name( with func_name( |
| 146 | + query = pgCatalogFuncRegex.ReplaceAllString(query, "$1(") |
| 147 | + |
| 148 | + // Replace OPERATOR(pg_catalog.~) with just ~ |
| 149 | + query = operatorRegex.ReplaceAllString(query, "$1") |
| 150 | + |
| 151 | + // Remove COLLATE pg_catalog.default |
| 152 | + query = collateRegex.ReplaceAllString(query, "") |
| 153 | + |
| 154 | + // Replace pg_catalog.pg_database with pg_database (our view in main schema) |
| 155 | + query = pgDatabaseRegex.ReplaceAllString(query, "pg_database") |
| 156 | + |
| 157 | + // Replace PostgreSQL type casts (order matters - most specific first) |
| 158 | + query = regtypeTextCastRegex.ReplaceAllString(query, "::VARCHAR") |
| 159 | + query = regtypeCastRegex.ReplaceAllString(query, "::VARCHAR") |
| 160 | + query = textCastRegex.ReplaceAllString(query, "::VARCHAR") |
| 161 | + |
| 162 | + return query |
| 163 | +} |
| 164 | + |
0 commit comments