true-pg 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +12 -8
  2. package/lib/bin.js +3 -7
  3. package/lib/extractor/adapter.d.ts +15 -0
  4. package/lib/extractor/adapter.js +53 -0
  5. package/lib/extractor/canonicalise.d.ts +64 -0
  6. package/lib/extractor/canonicalise.js +245 -0
  7. package/lib/extractor/fetchExtensionItemIds.d.ts +12 -0
  8. package/lib/extractor/fetchExtensionItemIds.js +43 -0
  9. package/lib/extractor/fetchTypes.d.ts +4 -0
  10. package/lib/extractor/fetchTypes.js +65 -0
  11. package/lib/extractor/index.d.ts +95 -0
  12. package/lib/extractor/index.js +140 -0
  13. package/lib/extractor/kinds/composite.d.ts +15 -0
  14. package/lib/extractor/kinds/composite.js +13 -0
  15. package/lib/extractor/kinds/domain.d.ts +15 -0
  16. package/lib/extractor/kinds/domain.js +13 -0
  17. package/lib/extractor/kinds/enum.d.ts +9 -0
  18. package/lib/extractor/kinds/enum.js +20 -0
  19. package/lib/extractor/kinds/function.d.ts +73 -0
  20. package/lib/extractor/kinds/function.js +179 -0
  21. package/lib/extractor/kinds/materialized-view.d.ts +17 -0
  22. package/lib/extractor/kinds/materialized-view.js +64 -0
  23. package/lib/extractor/kinds/parts/commentMapQueryPart.d.ts +2 -0
  24. package/lib/extractor/kinds/parts/commentMapQueryPart.js +14 -0
  25. package/lib/extractor/kinds/parts/indexMapQueryPart.d.ts +2 -0
  26. package/lib/extractor/kinds/parts/indexMapQueryPart.js +27 -0
  27. package/lib/extractor/kinds/range.d.ts +15 -0
  28. package/lib/extractor/kinds/range.js +13 -0
  29. package/lib/extractor/kinds/table.d.ts +212 -0
  30. package/lib/extractor/kinds/table.js +217 -0
  31. package/lib/extractor/kinds/util/parseInlineTable.d.ts +9 -0
  32. package/lib/extractor/kinds/util/parseInlineTable.js +135 -0
  33. package/lib/extractor/kinds/util/parseInlineTable.test.d.ts +1 -0
  34. package/lib/extractor/kinds/util/parseInlineTable.test.js +26 -0
  35. package/lib/extractor/kinds/view.d.ts +17 -0
  36. package/lib/extractor/kinds/view.js +63 -0
  37. package/lib/extractor/pgtype.d.ts +41 -0
  38. package/lib/extractor/pgtype.js +30 -0
  39. package/lib/index.d.ts +2 -2
  40. package/lib/index.js +38 -28
  41. package/lib/kysely/builtins.js +6 -4
  42. package/lib/kysely/index.js +103 -59
  43. package/lib/types.d.ts +38 -10
  44. package/lib/types.js +14 -3
  45. package/lib/util.d.ts +11 -0
  46. package/lib/util.js +6 -0
  47. package/lib/zod/builtins.js +11 -9
  48. package/lib/zod/index.js +107 -60
  49. package/package.json +3 -4
@@ -0,0 +1,212 @@
1
+ import { DbAdapter } from "../adapter.ts";
2
+ import type { PgType } from "../pgtype.ts";
3
+ import { Canonical } from "../canonicalise.ts";
4
+ export declare const updateActionMap: {
5
+ readonly a: "NO ACTION";
6
+ readonly r: "RESTRICT";
7
+ readonly c: "CASCADE";
8
+ readonly n: "SET NULL";
9
+ readonly d: "SET DEFAULT";
10
+ };
11
+ export type UpdateAction = (typeof updateActionMap)[keyof typeof updateActionMap];
12
+ /**
13
+ * Column reference.
14
+ */
15
+ export type ColumnReference = {
16
+ /**
17
+ * Schema name of the referenced table.
18
+ */
19
+ schemaName: string;
20
+ /**
21
+ * Table name of the referenced column.
22
+ */
23
+ tableName: string;
24
+ /**
25
+ * Name of the referenced column.
26
+ */
27
+ columnName: string;
28
+ /**
29
+ * Action to take on delete.
30
+ */
31
+ onDelete: UpdateAction;
32
+ /**
33
+ * Action to take on update.
34
+ */
35
+ onUpdate: UpdateAction;
36
+ /**
37
+ * Name of the foreign key constraint.
38
+ */
39
+ name: string;
40
+ };
41
+ /**
42
+ * Index for a column.
43
+ */
44
+ export type Index = {
45
+ /**
46
+ * Name of the index.
47
+ */
48
+ name: string;
49
+ /**
50
+ * Whether the index is a primary key.
51
+ */
52
+ isPrimary: boolean;
53
+ };
54
+ /**
55
+ * Check constraint on a table.
56
+ */
57
+ export interface TableCheck {
58
+ /**
59
+ * Name of the check constraint.
60
+ */
61
+ name: string;
62
+ /**
63
+ * Check constraint clause.
64
+ */
65
+ clause: string;
66
+ }
67
+ /**
68
+ * Column in a table.
69
+ */
70
+ export interface TableColumn {
71
+ /**
72
+ * Column name.
73
+ */
74
+ name: string;
75
+ /**
76
+ * Fully-detailed canonical type information
77
+ */
78
+ type: Canonical;
79
+ /**
80
+ * Comment on the column.
81
+ */
82
+ comment: string | null;
83
+ /**
84
+ * Default value of the column.
85
+ */
86
+ defaultValue: any;
87
+ /**
88
+ * Array of references from this column.
89
+ */
90
+ references: ColumnReference[];
91
+ /**
92
+ * Whether the column is nullable.
93
+ */
94
+ isNullable: boolean;
95
+ /**
96
+ * Whether the column is a primary key.
97
+ */
98
+ isPrimaryKey: boolean;
99
+ /**
100
+ * Behavior of the generated column. "ALWAYS" if always generated,
101
+ * "NEVER" if never generated, "BY DEFAULT" if generated when value
102
+ * is not provided.
103
+ */
104
+ generated: "ALWAYS" | "NEVER" | "BY DEFAULT";
105
+ /**
106
+ * Whether the column is updatable.
107
+ */
108
+ isUpdatable: boolean;
109
+ /**
110
+ * Whether the column is an identity column.
111
+ */
112
+ isIdentity: boolean;
113
+ /**
114
+ * Ordinal position of the column in the table. Starts from 1.
115
+ */
116
+ ordinalPosition: number;
117
+ }
118
+ /**
119
+ * Column in an index.
120
+ */
121
+ export interface TableIndexColumn {
122
+ /**
123
+ * Column name or null if functional index.
124
+ */
125
+ name: string | null;
126
+ /**
127
+ * Definition of index column.
128
+ */
129
+ definition: string;
130
+ }
131
+ /**
132
+ * Index on a table.
133
+ */
134
+ export interface TableIndex {
135
+ /**
136
+ * Name of the index.
137
+ */
138
+ name: string;
139
+ /**
140
+ * Whether the index is a primary key.
141
+ */
142
+ isPrimary: boolean;
143
+ /**
144
+ * Whether the index is unique.
145
+ */
146
+ isUnique: boolean;
147
+ /**
148
+ * Array of index columns in order.
149
+ */
150
+ columns: TableIndexColumn[];
151
+ }
152
+ /**
153
+ * Security policy on a table.
154
+ */
155
+ export interface TableSecurityPolicy {
156
+ /**
157
+ * Name of the security policy.
158
+ */
159
+ name: string;
160
+ /**
161
+ * Whether the policy is permissive.
162
+ */
163
+ isPermissive: boolean;
164
+ /**
165
+ * Array of roles the policy is applied to. ["public"] if applied to all
166
+ * roles.
167
+ */
168
+ rolesAppliedTo: string[];
169
+ /**
170
+ * Command type the policy applies to. "ALL" if all commands.
171
+ */
172
+ commandType: "ALL" | "SELECT" | "INSERT" | "UPDATE" | "DELETE";
173
+ /**
174
+ * Visibility expression of the policy specified by the USING clause.
175
+ */
176
+ visibilityExpression: string | null;
177
+ /**
178
+ * Modifiability expression of the policy specified by the WITH CHECK clause.
179
+ */
180
+ modifiabilityExpression: string | null;
181
+ }
182
+ /**
183
+ * Table in a schema.
184
+ */
185
+ export interface TableDetails extends PgType<"table"> {
186
+ /**
187
+ * Array of columns in the table.
188
+ */
189
+ columns: TableColumn[];
190
+ /**
191
+ * Array of indices in the table.
192
+ */
193
+ indices: TableIndex[];
194
+ /**
195
+ * Array of check constraints in the table.
196
+ */
197
+ checks: TableCheck[];
198
+ /**
199
+ * Whether row level security is enabled on the table.
200
+ */
201
+ isRowLevelSecurityEnabled: boolean;
202
+ /**
203
+ * Whether row level security is enforced on the table.
204
+ */
205
+ isRowLevelSecurityEnforced: boolean;
206
+ /**
207
+ * Array of security policies on the table.
208
+ */
209
+ securityPolicies: TableSecurityPolicy[];
210
+ }
211
+ declare const extractTable: (db: DbAdapter, table: PgType<"table">) => Promise<TableDetails>;
212
+ export default extractTable;
@@ -0,0 +1,217 @@
1
+ import { DbAdapter } from "../adapter.js";
2
+ import commentMapQueryPart from "./parts/commentMapQueryPart.js";
3
+ import indexMapQueryPart from "./parts/indexMapQueryPart.js";
4
+ import { Canonical, canonicalise } from "../canonicalise.js";
5
+ export const updateActionMap = {
6
+ a: "NO ACTION",
7
+ r: "RESTRICT",
8
+ c: "CASCADE",
9
+ n: "SET NULL",
10
+ d: "SET DEFAULT",
11
+ };
12
+ const referenceMapQueryPart = `
13
+ SELECT
14
+ source_attr.attname AS "column_name",
15
+ json_agg(json_build_object(
16
+ 'schemaName', expanded_constraint.target_schema,
17
+ 'tableName', expanded_constraint.target_table,
18
+ 'columnName', target_attr.attname,
19
+ 'onUpdate', case expanded_constraint.confupdtype
20
+ ${Object.entries(updateActionMap)
21
+ .map(([key, action]) => `when '${key}' then '${action}'`)
22
+ .join("\n")}
23
+ end,
24
+ 'onDelete', case expanded_constraint.confdeltype
25
+ ${Object.entries(updateActionMap)
26
+ .map(([key, action]) => `when '${key}' then '${action}'`)
27
+ .join("\n")}
28
+ end,
29
+ 'name', expanded_constraint.conname
30
+ )) AS references
31
+ FROM (
32
+ SELECT
33
+ unnest(conkey) AS "source_attnum",
34
+ unnest(confkey) AS "target_attnum",
35
+ target_namespace.nspname as "target_schema",
36
+ target_class.relname as "target_table",
37
+ confrelid,
38
+ conrelid,
39
+ conname,
40
+ confupdtype,
41
+ confdeltype
42
+ FROM
43
+ pg_constraint
44
+ JOIN pg_class source_class ON conrelid = source_class.oid
45
+ JOIN pg_namespace source_namespace ON source_class.relnamespace = source_namespace.oid
46
+
47
+ JOIN pg_class target_class ON confrelid = target_class.oid
48
+ JOIN pg_namespace target_namespace ON target_class.relnamespace = target_namespace.oid
49
+ WHERE
50
+ source_class.relname = $1
51
+ AND source_namespace.nspname = $2
52
+ AND contype = 'f') expanded_constraint
53
+ JOIN pg_attribute target_attr ON target_attr.attrelid = expanded_constraint.confrelid
54
+ AND target_attr.attnum = expanded_constraint.target_attnum
55
+ JOIN pg_attribute source_attr ON source_attr.attrelid = expanded_constraint.conrelid
56
+ AND source_attr.attnum = expanded_constraint.source_attnum
57
+ JOIN pg_class target_class ON target_class.oid = expanded_constraint.confrelid
58
+ WHERE
59
+ target_class.relispartition = FALSE
60
+ GROUP BY
61
+ source_attr.attname
62
+ `;
63
+ const extractTable = async (db, table) => {
64
+ const columnsQuery = await db.query(`
65
+ WITH
66
+ reference_map AS (
67
+ ${referenceMapQueryPart}
68
+ ),
69
+ index_map AS (
70
+ ${indexMapQueryPart}
71
+ ),
72
+ comment_map AS (
73
+ ${commentMapQueryPart}
74
+ )
75
+ SELECT
76
+ col.column_name AS "name",
77
+ format_type(attr.atttypid, attr.atttypmod)
78
+ || CASE
79
+ WHEN attr.attndims > 1 THEN repeat('[]', attr.attndims - 1)
80
+ ELSE ''
81
+ END AS "definedType",
82
+ comment_map.comment AS "comment",
83
+ col.column_default AS "defaultValue",
84
+ col.is_nullable = 'YES' AS "isNullable",
85
+ col.is_identity = 'YES' AS "isIdentity",
86
+ col.is_updatable = 'YES' AS "isUpdatable",
87
+ col.ordinal_position AS "ordinalPosition",
88
+ CASE
89
+ WHEN col.is_identity = 'YES' THEN col.identity_generation
90
+ ELSE col.is_generated
91
+ END AS "generated",
92
+ COALESCE(index_map.is_primary, FALSE) AS "isPrimaryKey",
93
+ COALESCE(reference_map.references, '[]'::json) AS "references",
94
+ row_to_json(col.*) AS "informationSchemaValue"
95
+ FROM
96
+ information_schema.columns col
97
+ JOIN pg_class c ON col.table_name = c.relname
98
+ JOIN pg_namespace n ON c.relnamespace = n.oid AND col.table_schema = n.nspname
99
+ JOIN pg_attribute attr ON c.oid = attr.attrelid AND col.column_name = attr.attname
100
+ LEFT JOIN index_map ON index_map.column_name = col.column_name
101
+ LEFT JOIN reference_map ON reference_map.column_name = col.column_name
102
+ LEFT JOIN comment_map ON comment_map.column_name = col.column_name
103
+ WHERE
104
+ col.table_name = $1
105
+ AND col.table_schema = $2
106
+ AND NOT attr.attisdropped
107
+ ORDER BY col.ordinal_position;
108
+ `, [table.name, table.schemaName]);
109
+ // Get the expanded type names from the query result
110
+ const definedTypes = columnsQuery.map(row => row.definedType);
111
+ // Use canonicaliseTypes to get detailed type information
112
+ const canonicalTypes = await canonicalise(db, definedTypes);
113
+ // Combine the column information with the canonical type information
114
+ const columns = columnsQuery.map((row, index) => ({
115
+ name: row.name,
116
+ type: canonicalTypes[index],
117
+ comment: row.comment,
118
+ defaultValue: row.defaultValue,
119
+ isPrimaryKey: row.isPrimaryKey,
120
+ references: row.references,
121
+ ordinalPosition: row.ordinalPosition,
122
+ isNullable: row.isNullable,
123
+ isIdentity: row.isIdentity,
124
+ isUpdatable: row.isUpdatable,
125
+ generated: row.generated,
126
+ informationSchemaValue: row.informationSchemaValue,
127
+ }));
128
+ const indicesQuery = await db.query(`
129
+ WITH index_columns AS (
130
+ SELECT
131
+ ix.indexrelid,
132
+ json_agg(json_build_object(
133
+ 'name', a.attname,
134
+ 'definition', pg_get_indexdef(ix.indexrelid, keys.key_order::integer, true)
135
+ ) ORDER BY keys.key_order) AS columns
136
+ FROM
137
+ pg_index ix
138
+ CROSS JOIN unnest(ix.indkey) WITH ORDINALITY AS keys(key, key_order)
139
+ LEFT JOIN pg_attribute a ON ix.indrelid = a.attrelid AND key = a.attnum
140
+ GROUP BY ix.indexrelid, ix.indrelid
141
+ )
142
+ SELECT
143
+ i.relname AS "name",
144
+ ix.indisprimary AS "isPrimary",
145
+ ix.indisunique AS "isUnique",
146
+ index_columns.columns
147
+ FROM
148
+ pg_index ix
149
+ INNER JOIN pg_class i ON ix.indexrelid = i.oid
150
+ INNER JOIN pg_class t ON ix.indrelid = t.oid
151
+ INNER JOIN pg_namespace n ON t.relnamespace = n.oid
152
+ INNER JOIN index_columns ON ix.indexrelid = index_columns.indexrelid
153
+ WHERE
154
+ t.relname = $1
155
+ AND n.nspname = $2
156
+ `, [table.name, table.schemaName]);
157
+ const indices = indicesQuery;
158
+ const checkQuery = await db.query(`
159
+ SELECT
160
+ source_namespace.nspname as "schema",
161
+ source_class.relname as "table",
162
+ json_agg(json_build_object(
163
+ 'name', con.conname,
164
+ 'clause', SUBSTRING(pg_get_constraintdef(con.oid) FROM 7)
165
+ )) as checks
166
+ FROM
167
+ pg_constraint con,
168
+ pg_class source_class,
169
+ pg_namespace source_namespace
170
+ WHERE
171
+ source_class.relname = $1
172
+ AND source_namespace.nspname = $2
173
+ AND conrelid = source_class.oid
174
+ AND source_class.relnamespace = source_namespace.oid
175
+ AND con.contype = 'c'
176
+ GROUP BY source_namespace.nspname, source_class.relname;
177
+ `, [table.name, table.schemaName]);
178
+ const checks = checkQuery
179
+ .flatMap(row => row.checks)
180
+ .map(({ name, clause }) => {
181
+ const numberOfBrackets = clause.startsWith("((") && clause.endsWith("))") ? 2 : 1;
182
+ return {
183
+ name,
184
+ clause: clause.slice(numberOfBrackets, clause.length - numberOfBrackets),
185
+ };
186
+ });
187
+ const rlsQuery = await db.query(`
188
+ SELECT
189
+ c.relrowsecurity AS "isRowLevelSecurityEnabled",
190
+ c.relforcerowsecurity AS "isRowLevelSecurityEnforced",
191
+ coalesce(json_agg(json_build_object(
192
+ 'name', p.policyname,
193
+ 'isPermissive', p.permissive = 'PERMISSIVE',
194
+ 'rolesAppliedTo', p.roles,
195
+ 'commandType', p.cmd,
196
+ 'visibilityExpression', p.qual,
197
+ 'modifiabilityExpression', p.with_check
198
+ )) FILTER (WHERE p.policyname IS NOT NULL), '[]'::json) AS "securityPolicies"
199
+ FROM
200
+ pg_class c
201
+ INNER JOIN pg_namespace n ON c.relnamespace = n.oid
202
+ LEFT JOIN pg_policies p ON c.relname = p.tablename AND n.nspname = p.schemaname
203
+ WHERE
204
+ c.relname = $1
205
+ AND n.nspname = $2
206
+ GROUP BY c.relrowsecurity, c.relforcerowsecurity
207
+ `, [table.name, table.schemaName]);
208
+ const rls = rlsQuery[0];
209
+ return {
210
+ ...table,
211
+ indices,
212
+ checks,
213
+ columns,
214
+ ...rls,
215
+ };
216
+ };
217
+ export default extractTable;
@@ -0,0 +1,9 @@
1
+ interface TableColumn {
2
+ name: string;
3
+ type: string;
4
+ }
5
+ /**
6
+ * Parse a Postgres RETURNS TABLE() definition string to extract column name and type pairs
7
+ */
8
+ export declare function parsePostgresTableDefinition(tableDefinition: string): TableColumn[];
9
+ export {};
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Parse a Postgres RETURNS TABLE() definition string to extract column name and type pairs
3
+ */
4
+ export function parsePostgresTableDefinition(tableDefinition) {
5
+ // Check if we have a table definition
6
+ if (!tableDefinition || !tableDefinition.toLowerCase().trim().startsWith("table(")) {
7
+ throw new Error('Invalid table definition format. Expected string starting with "TABLE("');
8
+ }
9
+ // Extract the content inside the TABLE() parentheses
10
+ const tableContentMatch = tableDefinition.match(/TABLE\s*\(\s*(.*?)\s*\)$/is);
11
+ if (!tableContentMatch || !tableContentMatch[1]) {
12
+ return [];
13
+ }
14
+ const columnsDefinition = tableContentMatch[1];
15
+ const columns = [];
16
+ let currentPos = 0;
17
+ let columnStart = 0;
18
+ let parenLevel = 0;
19
+ let inQuotes = false;
20
+ let quoteChar = null;
21
+ let escaping = false;
22
+ const len = columnsDefinition.length;
23
+ while (currentPos <= columnsDefinition.length) {
24
+ // add a virtual comma to the end
25
+ const char = currentPos === len ? "," : columnsDefinition[currentPos];
26
+ if (escaping) {
27
+ escaping = false;
28
+ currentPos++;
29
+ continue;
30
+ }
31
+ if (inQuotes) {
32
+ if (char === "\\") {
33
+ escaping = true;
34
+ currentPos++;
35
+ continue;
36
+ }
37
+ if (char === quoteChar) {
38
+ inQuotes = false;
39
+ }
40
+ currentPos++;
41
+ continue;
42
+ }
43
+ if (char === '"' || char === "'") {
44
+ inQuotes = true;
45
+ quoteChar = char;
46
+ currentPos++;
47
+ continue;
48
+ }
49
+ // Track parentheses nesting level
50
+ if (char === "(") {
51
+ parenLevel++;
52
+ }
53
+ else if (char === ")") {
54
+ parenLevel--;
55
+ }
56
+ // At column boundary (comma outside of any parentheses or quotes)
57
+ // or at the end of the string (when we add the virtual comma)
58
+ if (char === "," && parenLevel === 0 && !inQuotes && currentPos >= columnStart) {
59
+ const columnDef = columnsDefinition.substring(columnStart, currentPos).trim();
60
+ if (columnDef) {
61
+ try {
62
+ columns.push(parseColumnDefinition(columnDef));
63
+ }
64
+ catch (error) {
65
+ console.warn(`Skipping malformed column definition: ${columnDef}`, error);
66
+ }
67
+ }
68
+ columnStart = currentPos + 1;
69
+ }
70
+ currentPos++;
71
+ }
72
+ return columns;
73
+ }
74
+ const unquote = (str) => {
75
+ if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
76
+ return str
77
+ .substring(1, str.length - 1)
78
+ .replace(/\\"/g, '"')
79
+ .replace(/\\'/g, "'");
80
+ }
81
+ return str;
82
+ };
83
+ /**
84
+ * Parse a single column definition like "id integer" or "\"User Name\" text"
85
+ */
86
+ function parseColumnDefinition(columnDef) {
87
+ let pos = 0;
88
+ let inQuotes = false;
89
+ let quoteChar = null;
90
+ let escaping = false;
91
+ let nameEndPos = -1;
92
+ while (pos < columnDef.length) {
93
+ const char = columnDef[pos];
94
+ if (escaping) {
95
+ escaping = false;
96
+ pos++;
97
+ continue;
98
+ }
99
+ if (inQuotes) {
100
+ if (char === "\\") {
101
+ escaping = true;
102
+ }
103
+ else if (char === quoteChar) {
104
+ inQuotes = false;
105
+ }
106
+ pos++;
107
+ continue;
108
+ }
109
+ if (char === '"' || char === "'") {
110
+ inQuotes = true;
111
+ quoteChar = char;
112
+ pos++;
113
+ continue;
114
+ }
115
+ // Found whitespace outside quotes - this is where the name ends
116
+ if (/\s/.test(char) && !inQuotes) {
117
+ nameEndPos = pos;
118
+ break;
119
+ }
120
+ pos++;
121
+ }
122
+ if (nameEndPos === -1) {
123
+ throw new Error(`Could not parse column definition: ${columnDef}`);
124
+ }
125
+ // Extract the column name, removing quotes if present
126
+ let name = columnDef.substring(0, nameEndPos).trim();
127
+ name = unquote(name);
128
+ // Everything after the column name is the type
129
+ const type = columnDef.substring(nameEndPos).trim();
130
+ return { name, type };
131
+ }
132
+ // Example usage with complex types
133
+ // const tableDefinition = 'TABLE("id" integer, "User Name" text, "complex field" varchar(255)[], "nested type" decimal(10,2), tags text[], "quoted""identifier" json)';
134
+ // const columns = parsePostgresTableDefinition(tableDefinition);
135
+ // console.log(columns);
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { parsePostgresTableDefinition } from "./parseInlineTable.js";
3
+ describe("parsePostgresTableDefinition", () => {
4
+ it("should parse a table definition with a single column", () => {
5
+ const tableDefinition = "TABLE(id integer)";
6
+ const expectedColumns = [{ name: "id", type: "integer" }];
7
+ expect(parsePostgresTableDefinition(tableDefinition)).toEqual(expectedColumns);
8
+ });
9
+ it("should parse a table definition with a fully qualified column type", () => {
10
+ const tableDefinition = 'TABLE("Complex Type" "schema"."type")';
11
+ const expectedColumns = [{ name: "Complex Type", type: '"schema"."type"' }];
12
+ expect(parsePostgresTableDefinition(tableDefinition)).toEqual(expectedColumns);
13
+ });
14
+ it("should parse a complex table definition", () => {
15
+ const tableDefinition = 'TABLE("id" integer, "User Name" text, "complex field" varchar(255)[], "nested type" decimal(10,2), tags text[], "quoted\\"identifier" json)';
16
+ const expectedColumns = [
17
+ { name: "id", type: "integer" },
18
+ { name: "User Name", type: "text" },
19
+ { name: "complex field", type: "varchar(255)[]" },
20
+ { name: "nested type", type: "decimal(10,2)" },
21
+ { name: "tags", type: "text[]" },
22
+ { name: 'quoted"identifier', type: "json" },
23
+ ];
24
+ expect(parsePostgresTableDefinition(tableDefinition)).toEqual(expectedColumns);
25
+ });
26
+ });
@@ -0,0 +1,17 @@
1
+ import type { DbAdapter } from "../adapter.ts";
2
+ import type { PgType } from "../pgtype.ts";
3
+ import { Canonical } from "../canonicalise.ts";
4
+ export interface ViewColumn {
5
+ name: string;
6
+ type: Canonical;
7
+ isNullable: boolean;
8
+ isUpdatable: boolean;
9
+ ordinalPosition: number;
10
+ }
11
+ export interface ViewDetails extends PgType<"view"> {
12
+ columns: ViewColumn[];
13
+ isUpdatable: boolean;
14
+ checkOption: "NONE" | "LOCAL" | "CASCADED";
15
+ }
16
+ declare const extractView: (db: DbAdapter, view: PgType<"view">) => Promise<ViewDetails>;
17
+ export default extractView;
@@ -0,0 +1,63 @@
1
+ import { Canonical, canonicalise } from "../canonicalise.js";
2
+ const extractView = async (db, view) => {
3
+ // 1. Query for columns (information_schema.columns + pg_attribute)
4
+ const columnQuery = await db.query(`
5
+ SELECT
6
+ col.column_name AS "name",
7
+ format_type(attr.atttypid, attr.atttypmod)
8
+ || CASE
9
+ WHEN attr.attndims > 1 THEN repeat('[]', attr.attndims - 1)
10
+ ELSE ''
11
+ END AS "definedType",
12
+ col.is_nullable = 'YES' AS "isNullable",
13
+ col.is_updatable = 'YES' AS "isUpdatable",
14
+ col.ordinal_position AS "ordinalPosition"
15
+ FROM
16
+ information_schema.columns col
17
+ JOIN pg_class c ON col.table_name = c.relname AND c.relkind = 'v'
18
+ JOIN pg_namespace n ON c.relnamespace = n.oid AND col.table_schema = n.nspname
19
+ JOIN pg_attribute attr ON c.oid = attr.attrelid AND col.column_name = attr.attname
20
+ WHERE
21
+ col.table_name = $1
22
+ AND col.table_schema = $2
23
+ AND NOT attr.attisdropped -- Exclude dropped columns
24
+ ORDER BY col.ordinal_position;
25
+ `, [view.name, view.schemaName]);
26
+ // 2. Get canonical types
27
+ const definedTypes = columnQuery.map(row => row.definedType);
28
+ const canonicalTypes = await canonicalise(db, definedTypes);
29
+ const columns = columnQuery.map((row, index) => ({
30
+ name: row.name,
31
+ type: canonicalTypes[index],
32
+ isNullable: row.isNullable,
33
+ isUpdatable: row.isUpdatable,
34
+ ordinalPosition: row.ordinalPosition,
35
+ }));
36
+ // 3. Query for view definition, comment, and other properties
37
+ const viewInfoQuery = await db.query(`
38
+ SELECT
39
+ d.description AS "comment",
40
+ v.is_updatable = 'YES' AS "isUpdatable",
41
+ v.check_option AS "checkOption"
42
+ FROM
43
+ information_schema.views v
44
+ JOIN pg_catalog.pg_class c ON v.table_name = c.relname AND c.relkind = 'v'
45
+ JOIN pg_catalog.pg_namespace n ON c.relnamespace = n.oid AND v.table_schema = n.nspname
46
+ LEFT JOIN pg_catalog.pg_description d ON c.oid = d.objoid AND d.objsubid = 0
47
+ WHERE
48
+ v.table_name = $1
49
+ AND v.table_schema = $2;
50
+ `, [view.name, view.schemaName]);
51
+ const viewInfo = viewInfoQuery[0];
52
+ if (!viewInfo) {
53
+ throw new Error(`Could not find view "${view.schemaName}"."${view.name}".`);
54
+ }
55
+ return {
56
+ ...view,
57
+ columns,
58
+ comment: viewInfo.comment ?? view.comment,
59
+ isUpdatable: viewInfo.isUpdatable,
60
+ checkOption: viewInfo.checkOption,
61
+ };
62
+ };
63
+ export default extractView;