true-pg 0.2.3 → 0.3.0
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.
- package/lib/bin.js +3 -7
- package/lib/extractor/adapter.d.ts +15 -0
- package/lib/extractor/adapter.js +53 -0
- package/lib/extractor/canonicalise.d.ts +64 -0
- package/lib/extractor/canonicalise.js +245 -0
- package/lib/extractor/fetchExtensionItemIds.d.ts +12 -0
- package/lib/extractor/fetchExtensionItemIds.js +43 -0
- package/lib/extractor/fetchTypes.d.ts +4 -0
- package/lib/extractor/fetchTypes.js +65 -0
- package/lib/extractor/index.d.ts +95 -0
- package/lib/extractor/index.js +140 -0
- package/lib/extractor/kinds/composite.d.ts +15 -0
- package/lib/extractor/kinds/composite.js +13 -0
- package/lib/extractor/kinds/domain.d.ts +15 -0
- package/lib/extractor/kinds/domain.js +13 -0
- package/lib/extractor/kinds/enum.d.ts +9 -0
- package/lib/extractor/kinds/enum.js +20 -0
- package/lib/extractor/kinds/function.d.ts +73 -0
- package/lib/extractor/kinds/function.js +179 -0
- package/lib/extractor/kinds/materialized-view.d.ts +17 -0
- package/lib/extractor/kinds/materialized-view.js +64 -0
- package/lib/extractor/kinds/parts/commentMapQueryPart.d.ts +2 -0
- package/lib/extractor/kinds/parts/commentMapQueryPart.js +14 -0
- package/lib/extractor/kinds/parts/indexMapQueryPart.d.ts +2 -0
- package/lib/extractor/kinds/parts/indexMapQueryPart.js +27 -0
- package/lib/extractor/kinds/range.d.ts +15 -0
- package/lib/extractor/kinds/range.js +13 -0
- package/lib/extractor/kinds/table.d.ts +212 -0
- package/lib/extractor/kinds/table.js +217 -0
- package/lib/extractor/kinds/util/parseInlineTable.d.ts +9 -0
- package/lib/extractor/kinds/util/parseInlineTable.js +135 -0
- package/lib/extractor/kinds/util/parseInlineTable.test.d.ts +1 -0
- package/lib/extractor/kinds/util/parseInlineTable.test.js +26 -0
- package/lib/extractor/kinds/view.d.ts +17 -0
- package/lib/extractor/kinds/view.js +63 -0
- package/lib/extractor/pgtype.d.ts +41 -0
- package/lib/extractor/pgtype.js +30 -0
- package/lib/index.d.ts +2 -2
- package/lib/index.js +38 -28
- package/lib/kysely/builtins.js +6 -4
- package/lib/kysely/index.js +103 -59
- package/lib/types.d.ts +38 -10
- package/lib/types.js +14 -3
- package/lib/util.d.ts +11 -0
- package/lib/util.js +6 -0
- package/lib/zod/builtins.js +11 -9
- package/lib/zod/index.js +107 -60
- 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 @@
|
|
1
|
+
export {};
|
@@ -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;
|