@supabase/postgrest-typegen 0.2.1 → 0.2.3

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 (58) hide show
  1. package/README.md +13 -5
  2. package/dist/collation.d.ts +2 -0
  3. package/dist/collation.js +15 -0
  4. package/dist/generation/index.d.ts +1 -1
  5. package/dist/generation/python.js +21 -15
  6. package/dist/generation/swift.js +9 -2
  7. package/dist/generation/typescript.d.ts +18 -7
  8. package/dist/generation/typescript.js +102 -79
  9. package/dist/introspection/index.js +6 -23
  10. package/dist/introspection/relationships.d.ts +4 -4
  11. package/dist/introspection/relationships.js +4 -11
  12. package/dist/introspection/sql/columns.sql.d.ts +2 -7
  13. package/dist/introspection/sql/columns.sql.js +23 -17
  14. package/dist/introspection/sql/common.d.ts +7 -15
  15. package/dist/introspection/sql/foreign_tables.sql.d.ts +2 -6
  16. package/dist/introspection/sql/foreign_tables.sql.js +0 -5
  17. package/dist/introspection/sql/functions.sql.d.ts +2 -5
  18. package/dist/introspection/sql/functions.sql.js +0 -33
  19. package/dist/introspection/sql/materialized_views.sql.d.ts +2 -4
  20. package/dist/introspection/sql/materialized_views.sql.js +0 -5
  21. package/dist/introspection/sql/primary_keys.sql.d.ts +2 -4
  22. package/dist/introspection/sql/primary_keys.sql.js +0 -4
  23. package/dist/introspection/sql/schemas.sql.d.ts +2 -6
  24. package/dist/introspection/sql/schemas.sql.js +2 -6
  25. package/dist/introspection/sql/table.sql.d.ts +2 -4
  26. package/dist/introspection/sql/table.sql.js +0 -5
  27. package/dist/introspection/sql/table_relationships.sql.d.ts +2 -2
  28. package/dist/introspection/sql/types.sql.d.ts +5 -5
  29. package/dist/introspection/sql/types.sql.js +6 -17
  30. package/dist/introspection/sql/views.sql.d.ts +2 -4
  31. package/dist/introspection/sql/views.sql.js +0 -5
  32. package/dist/introspection/sql/views_key_dependencies.sql.d.ts +2 -2
  33. package/dist/introspection/sql/views_key_dependencies.sql.js +64 -75
  34. package/dist/sort.js +32 -16
  35. package/dist/types.d.ts +4 -4
  36. package/dist/types.js +6 -4
  37. package/package.json +4 -4
  38. package/src/collation.ts +16 -0
  39. package/src/generation/index.ts +1 -0
  40. package/src/generation/python.ts +28 -15
  41. package/src/generation/swift.ts +9 -2
  42. package/src/generation/typescript.ts +161 -104
  43. package/src/introspection/index.ts +7 -32
  44. package/src/introspection/relationships.ts +10 -19
  45. package/src/introspection/sql/columns.sql.ts +25 -26
  46. package/src/introspection/sql/common.ts +7 -17
  47. package/src/introspection/sql/foreign_tables.sql.ts +2 -13
  48. package/src/introspection/sql/functions.sql.ts +2 -44
  49. package/src/introspection/sql/materialized_views.sql.ts +2 -11
  50. package/src/introspection/sql/primary_keys.sql.ts +2 -10
  51. package/src/introspection/sql/schemas.sql.ts +4 -14
  52. package/src/introspection/sql/table.sql.ts +2 -11
  53. package/src/introspection/sql/table_relationships.sql.ts +2 -4
  54. package/src/introspection/sql/types.sql.ts +6 -26
  55. package/src/introspection/sql/views.sql.ts +2 -11
  56. package/src/introspection/sql/views_key_dependencies.sql.ts +83 -77
  57. package/src/sort.ts +37 -18
  58. package/src/types.ts +6 -4
@@ -1,18 +1,8 @@
1
- /**
2
- * Shared prop shapes for the SQL builders, ported from
3
- * `postgres-meta/src/lib/sql/common.ts`. Only the variants used by the
4
- * typegen introspection path are kept.
5
- */
6
- export type SQLQueryProps = {
7
- limit?: number;
8
- offset?: number;
9
- };
10
-
11
- export type SQLQueryPropsWithSchemaFilter = SQLQueryProps & {
12
- schemaFilter?: string;
13
- };
14
-
15
- export type SQLQueryPropsWithSchemaFilterAndIdsFilter = SQLQueryProps & {
16
- schemaFilter?: string;
17
- idsFilter?: string;
1
+ /** The only option the introspection passes to its SQL builders. */
2
+ export type SchemaFilterProps = {
3
+ /**
4
+ * An `IN (…)` or `NOT IN (…)` fragment from `filterByList`, or the empty
5
+ * string for no filter.
6
+ */
7
+ schemaFilter: string;
18
8
  };
@@ -1,13 +1,6 @@
1
- import { literal } from "./pg-format.ts";
2
- import type { SQLQueryProps } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
3
2
 
4
- export const FOREIGN_TABLES_SQL = (
5
- props: SQLQueryProps & {
6
- schemaFilter?: string;
7
- idsFilter?: string;
8
- tableIdentifierFilter?: string;
9
- },
10
- ) => /* SQL */ `
3
+ export const FOREIGN_TABLES_SQL = (props: SchemaFilterProps) => /* SQL */ `
11
4
  SELECT
12
5
  c.oid :: int8 AS id,
13
6
  n.nspname AS schema,
@@ -18,9 +11,5 @@ FROM
18
11
  JOIN pg_namespace n ON n.oid = c.relnamespace
19
12
  WHERE
20
13
  ${props.schemaFilter ? `n.nspname ${props.schemaFilter} AND` : ""}
21
- ${props.idsFilter ? `c.oid ${props.idsFilter} AND` : ""}
22
- ${props.tableIdentifierFilter ? `(n.nspname || '.' || c.relname) ${props.tableIdentifierFilter} AND` : ""}
23
14
  c.relkind = 'f'
24
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
25
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
26
15
  `;
@@ -1,15 +1,6 @@
1
- // oxlint-disable typescript/restrict-template-expressions -- verbatim port of postgres-meta's
2
- // builder, which interpolates the `args` string[] into the SQL (relying on Array#toString). The
3
- // expression sits inside a template literal where a targeted disable comment would corrupt the SQL.
4
- import { literal } from "./pg-format.ts";
5
- import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
6
2
 
7
- export const FUNCTIONS_SQL = (
8
- props: SQLQueryPropsWithSchemaFilterAndIdsFilter & {
9
- nameFilter?: string;
10
- args?: string[];
11
- },
12
- ) => /* SQL */ `
3
+ export const FUNCTIONS_SQL = (props: SchemaFilterProps) => /* SQL */ `
13
4
  -- CTE with sane arg_modes, arg_names, and arg_types.
14
5
  -- All three are always of the same length.
15
6
  -- All three include all args, including OUT and TABLE args.
@@ -36,37 +27,6 @@ with functions as (
36
27
  ${props.schemaFilter ? `join pg_namespace n on p.pronamespace = n.oid` : ""}
37
28
  where
38
29
  ${props.schemaFilter ? `n.nspname ${props.schemaFilter} AND` : ""}
39
- ${props.idsFilter ? `p.oid ${props.idsFilter} AND` : ""}
40
- ${props.nameFilter ? `p.proname ${props.nameFilter} AND` : ""}
41
- ${
42
- props.args === undefined
43
- ? ""
44
- : props.args.length > 0
45
- ? `p.proargtypes::text = ${
46
- props.args.length
47
- ? `(
48
- SELECT STRING_AGG(type_oid::text, ' ') FROM (
49
- SELECT (
50
- split_args.arr[
51
- array_length(
52
- split_args.arr,
53
- 1
54
- )
55
- ]::regtype::oid
56
- ) AS type_oid FROM (
57
- SELECT STRING_TO_ARRAY(
58
- UNNEST(
59
- ARRAY[${props.args}]
60
- ),
61
- ' '
62
- ) AS arr
63
- ) AS split_args
64
- ) args
65
- )`
66
- : "''"
67
- } AND`
68
- : ""
69
- }
70
30
  p.prokind = 'f'
71
31
  )
72
32
  select
@@ -154,6 +114,4 @@ from
154
114
  group by
155
115
  t1.oid
156
116
  ) f_args on f_args.oid = f.oid
157
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
158
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
159
117
  `;
@@ -1,11 +1,6 @@
1
- import { literal } from "./pg-format.ts";
2
- import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
3
2
 
4
- export const MATERIALIZED_VIEWS_SQL = (
5
- props: SQLQueryPropsWithSchemaFilterAndIdsFilter & {
6
- materializedViewIdentifierFilter?: string;
7
- },
8
- ) => /* SQL */ `
3
+ export const MATERIALIZED_VIEWS_SQL = (props: SchemaFilterProps) => /* SQL */ `
9
4
  select
10
5
  c.oid::int8 as id,
11
6
  n.nspname as schema,
@@ -17,9 +12,5 @@ from
17
12
  join pg_namespace n on n.oid = c.relnamespace
18
13
  where
19
14
  ${props.schemaFilter ? `n.nspname ${props.schemaFilter} AND` : ""}
20
- ${props.idsFilter ? `c.oid ${props.idsFilter} AND` : ""}
21
- ${props.materializedViewIdentifierFilter ? `(n.nspname || '.' || c.relname) ${props.materializedViewIdentifierFilter} AND` : ""}
22
15
  c.relkind = 'm'
23
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
24
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
25
16
  `;
@@ -1,11 +1,6 @@
1
- import { literal } from "./pg-format.ts";
2
- import type { SQLQueryPropsWithSchemaFilter } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
3
2
 
4
- export const PRIMARY_KEYS_SQL = (
5
- props: SQLQueryPropsWithSchemaFilter & {
6
- tableIdentifierFilter?: string;
7
- },
8
- ) => /* SQL */ `
3
+ export const PRIMARY_KEYS_SQL = (props: SchemaFilterProps) => /* SQL */ `
9
4
  SELECT
10
5
  c.oid :: int8 AS table_id,
11
6
  n.nspname AS schema,
@@ -18,7 +13,6 @@ FROM
18
13
  JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(i.indkey)
19
14
  WHERE
20
15
  ${props.schemaFilter ? `n.nspname ${props.schemaFilter} AND` : ""}
21
- ${props.tableIdentifierFilter ? `n.nspname || '.' || c.relname ${props.tableIdentifierFilter} AND` : ""}
22
16
  i.indisprimary
23
17
  AND c.relkind IN ('r', 'p')
24
18
  AND NOT pg_is_other_temp_schema(n.oid)
@@ -33,6 +27,4 @@ WHERE
33
27
  ORDER BY
34
28
  c.oid,
35
29
  array_position(i.indkey, a.attnum)
36
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
37
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
38
30
  `;
@@ -1,13 +1,6 @@
1
- import { literal } from "./pg-format.ts";
2
- import type { SQLQueryProps } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
3
2
 
4
- export const SCHEMAS_SQL = (
5
- props: SQLQueryProps & {
6
- nameFilter?: string;
7
- idsFilter?: string;
8
- includeSystemSchemas?: boolean;
9
- },
10
- ) => /* SQL */ `
3
+ export const SCHEMAS_SQL = (props: SchemaFilterProps) => /* SQL */ `
11
4
  -- Adapted from information_schema.schemata
12
5
  select
13
6
  n.oid::int8 as id,
@@ -18,15 +11,12 @@ from
18
11
  pg_roles u
19
12
  where
20
13
  n.nspowner = u.oid
21
- ${props.idsFilter ? `and n.oid ${props.idsFilter}` : ""}
22
- ${props.nameFilter ? `and n.nspname ${props.nameFilter}` : ""}
23
- ${!props.includeSystemSchemas ? `and not pg_catalog.starts_with(n.nspname, 'pg_')` : ""}
14
+ ${props.schemaFilter ? `and n.nspname ${props.schemaFilter}` : ""}
15
+ and not pg_catalog.starts_with(n.nspname, 'pg_')
24
16
  and (
25
17
  pg_has_role(n.nspowner, 'USAGE')
26
18
  or has_schema_privilege(n.oid, 'CREATE, USAGE')
27
19
  )
28
20
  and not pg_catalog.starts_with(n.nspname, 'pg_temp_')
29
21
  and not pg_catalog.starts_with(n.nspname, 'pg_toast_temp_')
30
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
31
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
32
22
  `;
@@ -1,11 +1,6 @@
1
- import { literal } from "./pg-format.ts";
2
- import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
3
2
 
4
- export const TABLES_SQL = (
5
- props: SQLQueryPropsWithSchemaFilterAndIdsFilter & {
6
- tableIdentifierFilter?: string;
7
- },
8
- ) => /* SQL */ `
3
+ export const TABLES_SQL = (props: SchemaFilterProps) => /* SQL */ `
9
4
  SELECT
10
5
  c.oid :: int8 AS id,
11
6
  nc.nspname AS schema,
@@ -30,8 +25,6 @@ FROM
30
25
  JOIN pg_class c ON nc.oid = c.relnamespace
31
26
  WHERE
32
27
  ${props.schemaFilter ? `nc.nspname ${props.schemaFilter} AND` : ""}
33
- ${props.idsFilter ? `c.oid ${props.idsFilter} AND` : ""}
34
- ${props.tableIdentifierFilter ? `nc.nspname || '.' || c.relname ${props.tableIdentifierFilter} AND` : ""}
35
28
  c.relkind IN ('r', 'p')
36
29
  AND NOT pg_is_other_temp_schema(nc.oid)
37
30
  AND (
@@ -42,6 +35,4 @@ WHERE
42
35
  )
43
36
  OR has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')
44
37
  )
45
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
46
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
47
38
  `;
@@ -1,8 +1,6 @@
1
- import type { SQLQueryPropsWithSchemaFilter } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
2
2
 
3
- export const TABLE_RELATIONSHIPS_SQL = (
4
- props: SQLQueryPropsWithSchemaFilter,
5
- ) => /* SQL */ `
3
+ export const TABLE_RELATIONSHIPS_SQL = (props: SchemaFilterProps) => /* SQL */ `
6
4
  -- Adapted from
7
5
  -- https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L722
8
6
  WITH
@@ -1,12 +1,8 @@
1
- import { literal } from "./pg-format.ts";
2
- import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
3
-
4
- export const TYPES_SQL = (
5
- props: SQLQueryPropsWithSchemaFilterAndIdsFilter & {
6
- includeTableTypes?: boolean;
7
- includeArrayTypes?: boolean;
8
- },
9
- ) => /* SQL */ `
1
+ /**
2
+ * Every type of every schema, including the row types of tables and views
3
+ * and the array types, since a column of any schema may use them.
4
+ */
5
+ export const TYPES_SQL = /* SQL */ `
10
6
  select
11
7
  t.oid::int8 as id,
12
8
  t.typname as name,
@@ -48,27 +44,11 @@ from
48
44
  t.typrelid = 0
49
45
  or (
50
46
  select
51
- c.relkind ${props.includeTableTypes ? `in ('c', 'r', 'v', 'm', 'p')` : `= 'c'`}
47
+ c.relkind in ('c', 'r', 'v', 'm', 'p', 'f')
52
48
  from
53
49
  pg_class c
54
50
  where
55
51
  c.oid = t.typrelid
56
52
  )
57
53
  )
58
- ${
59
- !props.includeArrayTypes
60
- ? `and not exists (
61
- select
62
- from
63
- pg_type el
64
- where
65
- el.oid = t.typelem
66
- and el.typarray = t.oid
67
- )`
68
- : ""
69
- }
70
- ${props.schemaFilter ? `and n.nspname ${props.schemaFilter}` : ""}
71
- ${props.idsFilter ? `and t.oid ${props.idsFilter}` : ""}
72
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
73
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
74
54
  `;
@@ -1,11 +1,6 @@
1
- import { literal } from "./pg-format.ts";
2
- import type { SQLQueryPropsWithSchemaFilterAndIdsFilter } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
3
2
 
4
- export const VIEWS_SQL = (
5
- props: SQLQueryPropsWithSchemaFilterAndIdsFilter & {
6
- viewIdentifierFilter?: string;
7
- },
8
- ) => /* SQL */ `
3
+ export const VIEWS_SQL = (props: SchemaFilterProps) => /* SQL */ `
9
4
  SELECT
10
5
  c.oid :: int8 AS id,
11
6
  n.nspname AS schema,
@@ -24,9 +19,5 @@ FROM
24
19
  JOIN pg_namespace n ON n.oid = c.relnamespace
25
20
  WHERE
26
21
  ${props.schemaFilter ? `n.nspname ${props.schemaFilter} AND` : ""}
27
- ${props.idsFilter ? `c.oid ${props.idsFilter} AND` : ""}
28
- ${props.viewIdentifierFilter ? `(n.nspname || '.' || c.relname) ${props.viewIdentifierFilter} AND` : ""}
29
22
  c.relkind = 'v'
30
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
31
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
32
23
  `;
@@ -1,7 +1,8 @@
1
- import type { SQLQueryPropsWithSchemaFilter } from "./common.ts";
1
+ import type { SchemaFilterProps } from "./common.ts";
2
+ import { literal } from "./pg-format.ts";
2
3
 
3
4
  export const VIEWS_KEY_DEPENDENCIES_SQL = (
4
- props: SQLQueryPropsWithSchemaFilter,
5
+ props: SchemaFilterProps,
5
6
  ) => /* SQL */ `
6
7
  -- Adapted from
7
8
  -- https://github.com/PostgREST/postgrest/blob/f9f0f79fa914ac00c11fbf7f4c558e14821e67e2/src/PostgREST/SchemaCache.hs#L820
@@ -47,81 +48,7 @@ views as (
47
48
  transform_json as (
48
49
  select
49
50
  view_id, view_schema, view_name,
50
- -- the following formatting is without indentation on purpose
51
- -- to allow simple diffs, with less whitespace noise
52
- replace(
53
- replace(
54
- replace(
55
- replace(
56
- replace(
57
- replace(
58
- replace(
59
- regexp_replace(
60
- replace(
61
- replace(
62
- replace(
63
- replace(
64
- replace(
65
- replace(
66
- replace(
67
- replace(
68
- replace(
69
- replace(
70
- replace(
71
- view_definition::text,
72
- -- This conversion to json is heavily optimized for performance.
73
- -- The general idea is to use as few regexp_replace() calls as possible.
74
- -- Simple replace() is a lot faster, so we jump through some hoops
75
- -- to be able to use regexp_replace() only once.
76
- -- This has been tested against a huge schema with 250+ different views.
77
- -- The unit tests do NOT reflect all possible inputs. Be careful when changing this!
78
- -- -----------------------------------------------
79
- -- pattern | replacement | flags
80
- -- -----------------------------------------------
81
- -- <> in pg_node_tree is the same as null in JSON, but due to very poor performance of json_typeof
82
- -- we need to make this an empty array here to prevent json_array_elements from throwing an error
83
- -- when the targetList is null.
84
- -- We'll need to put it first, to make the node protection below work for node lists that start with
85
- -- null: (<> ..., too. This is the case for coldefexprs, when the first column does not have a default value.
86
- '<>' , '()'
87
- -- , is not part of the pg_node_tree format, but used in the regex.
88
- -- This removes all , that might be part of column names.
89
- ), ',' , ''
90
- -- The same applies for { and }, although those are used a lot in pg_node_tree.
91
- -- We remove the escaped ones, which might be part of column names again.
92
- ), E'\\\\{' , ''
93
- ), E'\\\\}' , ''
94
- -- The fields we need are formatted as json manually to protect them from the regex.
95
- ), ' :targetList ' , ',"targetList":'
96
- ), ' :resno ' , ',"resno":'
97
- ), ' :resorigtbl ' , ',"resorigtbl":'
98
- ), ' :resorigcol ' , ',"resorigcol":'
99
- -- Make the regex also match the node type, e.g. \`{QUERY ...\`, to remove it in one pass.
100
- ), '{' , '{ :'
101
- -- Protect node lists, which start with \`({\` or \`((\` from the greedy regex.
102
- -- The extra \`{\` is removed again later.
103
- ), '((' , '{(('
104
- ), '({' , '{({'
105
- -- This regex removes all unused fields to avoid the need to format all of them correctly.
106
- -- This leads to a smaller json result as well.
107
- -- Removal stops at \`,\` for used fields (see above) and \`}\` for the end of the current node.
108
- -- Nesting can't be parsed correctly with a regex, so we stop at \`{\` as well and
109
- -- add an empty key for the followig node.
110
- ), ' :[^}{,]+' , ',"":' , 'g'
111
- -- For performance, the regex also added those empty keys when hitting a \`,\` or \`}\`.
112
- -- Those are removed next.
113
- ), ',"":}' , '}'
114
- ), ',"":,' , ','
115
- -- This reverses the "node list protection" from above.
116
- ), '{(' , '('
117
- -- Every key above has been added with a \`,\` so far. The first key in an object doesn't need it.
118
- ), '{,' , '{'
119
- -- pg_node_tree has \`()\` around lists, but JSON uses \`[]\`
120
- ), '(' , '['
121
- ), ')' , ']'
122
- -- pg_node_tree has \` \` between list items, but JSON uses \`,\`
123
- ), ' ' , ','
124
- )::json as view_definition
51
+ ${nodeTreeToJson("view_definition::text")}::json as view_definition
125
52
  from views
126
53
  ),
127
54
  target_entries as(
@@ -197,3 +124,82 @@ group by sch.nspname, tbl.relname, rep.view_schema, rep.view_name, pks_fks.conn
197
124
  -- make sure we only return key for which all columns are referenced in the view - no partial PKs or FKs
198
125
  having ncol = array_length(array_agg(row(col.attname, view_columns) order by pks_fks.ord), 1)
199
126
  `;
127
+
128
+ /**
129
+ * One text rewrite of the `pg_node_tree` to JSON conversion: a `replace`, or
130
+ * a global `regexp_replace` when `isRegex` is set.
131
+ */
132
+ type NodeTreeRewrite = {
133
+ pattern: string;
134
+ replacement: string;
135
+ isRegex?: boolean;
136
+ };
137
+
138
+ /**
139
+ * The rewrites, innermost first, that turn the `pg_node_tree` text of a view
140
+ * definition into JSON exposing only `targetList`, `resno`, `resorigtbl` and
141
+ * `resorigcol`. Ported from PostgREST's schema cache query, which keeps them
142
+ * as plain `replace` calls with a single `regexp_replace` for speed; the
143
+ * order matters, and the comments carry PostgREST's reasoning.
144
+ */
145
+ const NODE_TREE_TO_JSON_REWRITES: readonly NodeTreeRewrite[] = [
146
+ // `<>` is pg_node_tree's null. json_typeof is too slow to special-case it,
147
+ // so it becomes an empty list, which also keeps the node protection below
148
+ // working for lists that start with null, such as coldefexprs whose first
149
+ // column has no default.
150
+ { pattern: "<>", replacement: "()" },
151
+ // `,`, `{` and `}` are used by the JSON conversion; drop the ones that may
152
+ // be part of column names (`{` and `}` are escaped there).
153
+ { pattern: ",", replacement: "" },
154
+ { pattern: "\\{", replacement: "" },
155
+ { pattern: "\\}", replacement: "" },
156
+ // Format the fields we need as JSON keys so the regex below leaves them.
157
+ { pattern: " :targetList ", replacement: ',"targetList":' },
158
+ { pattern: " :resno ", replacement: ',"resno":' },
159
+ { pattern: " :resorigtbl ", replacement: ',"resorigtbl":' },
160
+ { pattern: " :resorigcol ", replacement: ',"resorigcol":' },
161
+ // Make the regex also match the node type, e.g. `{QUERY ...`.
162
+ { pattern: "{", replacement: "{ :" },
163
+ // Protect node lists, which start with `({` or `((`, from the greedy regex.
164
+ // The extra `{` is removed again below.
165
+ { pattern: "((", replacement: "{((" },
166
+ { pattern: "({", replacement: "{({" },
167
+ // Remove every unused field. Removal stops at `,` for the kept fields, at
168
+ // `}` for the end of the node and at `{` for a nested node, leaving an empty
169
+ // key for the following node.
170
+ { pattern: " :[^}{,]+", replacement: ',"":', isRegex: true },
171
+ // The regex also added those empty keys before `}` and `,`.
172
+ { pattern: ',"":}', replacement: "}" },
173
+ { pattern: ',"":,', replacement: "," },
174
+ // Undo the node list protection.
175
+ { pattern: "{(", replacement: "(" },
176
+ // Every key was added with a leading `,`; the first key of an object does
177
+ // not need it.
178
+ { pattern: "{,", replacement: "{" },
179
+ // pg_node_tree lists use `( )` with space separated items; JSON uses `[ ]`
180
+ // with commas.
181
+ { pattern: "(", replacement: "[" },
182
+ { pattern: ")", replacement: "]" },
183
+ { pattern: " ", replacement: "," },
184
+ ];
185
+
186
+ /**
187
+ * Render {@link NODE_TREE_TO_JSON_REWRITES} applied to `expression` as nested
188
+ * SQL calls: the opening calls stacked, then each rewrite's arguments on the
189
+ * line that closes it.
190
+ */
191
+ function nodeTreeToJson(expression: string): string {
192
+ const lines = NODE_TREE_TO_JSON_REWRITES.toReversed().map(
193
+ (rewrite) => ` ${rewrite.isRegex ? "regexp_replace" : "replace"}(`,
194
+ );
195
+ lines.push(` ${expression},`);
196
+ NODE_TREE_TO_JSON_REWRITES.forEach((rewrite, index) => {
197
+ const flags = rewrite.isRegex ? ", 'g'" : "";
198
+ const closing =
199
+ index === NODE_TREE_TO_JSON_REWRITES.length - 1 ? ")" : "),";
200
+ lines.push(
201
+ ` ${literal(rewrite.pattern)}, ${literal(rewrite.replacement)}${flags}${closing}`,
202
+ );
203
+ });
204
+ return lines.join("\n");
205
+ }
package/src/sort.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  * addressed by name (the generated type is an object), matching TypeScript.
27
27
  */
28
28
  import type { GeneratorMetadata, PostgresRelationship } from "./types.ts";
29
+ import { compareStrings } from "./collation.ts";
29
30
 
30
31
  // Relations (tables/views/materialized views/foreign tables) and types share a
31
32
  // (schema, name) identity within a database; `id` breaks any residual tie.
@@ -33,20 +34,35 @@ const bySchemaName = (
33
34
  a: { schema: string; name: string; id: number },
34
35
  b: { schema: string; name: string; id: number },
35
36
  ): number =>
36
- a.schema.localeCompare(b.schema) ||
37
- a.name.localeCompare(b.name) ||
37
+ compareStrings(a.schema, b.schema) ||
38
+ compareStrings(a.name, b.name) ||
38
39
  a.id - b.id;
39
40
 
40
- // Mirrors the TypeScript generator's historical `relationships.sort`.
41
+ // Column lists compare as their JSON text, matching the TypeScript generator's
42
+ // historical `relationships.sort`.
43
+ const compareColumns = (a: string[], b: string[]): number =>
44
+ compareStrings(JSON.stringify(a), JSON.stringify(b));
45
+
46
+ // The first three keys mirror the TypeScript generator's historical
47
+ // `relationships.sort`. They are not total: `expandViewRelationships` in
48
+ // `introspection/relationships.ts` copies one foreign key onto every view
49
+ // exposing it, on either side, and onto every combination of view columns
50
+ // carrying it. View-to-table copies share the referenced side and differ in
51
+ // `schema`, `relation` and `columns`; table-to-view copies onto same-named
52
+ // views in different schemas differ only in `referenced_schema`. The remaining
53
+ // keys order those copies, since the view key dependency query does not order
54
+ // the view columns it aggregates.
41
55
  const byRelationship = (
42
56
  a: PostgresRelationship,
43
57
  b: PostgresRelationship,
44
58
  ): number =>
45
- a.foreign_key_name.localeCompare(b.foreign_key_name) ||
46
- a.referenced_relation.localeCompare(b.referenced_relation) ||
47
- JSON.stringify(a.referenced_columns).localeCompare(
48
- JSON.stringify(b.referenced_columns),
49
- );
59
+ compareStrings(a.foreign_key_name, b.foreign_key_name) ||
60
+ compareStrings(a.referenced_relation, b.referenced_relation) ||
61
+ compareColumns(a.referenced_columns, b.referenced_columns) ||
62
+ compareStrings(a.referenced_schema, b.referenced_schema) ||
63
+ compareStrings(a.schema, b.schema) ||
64
+ compareStrings(a.relation, b.relation) ||
65
+ compareColumns(a.columns, b.columns);
50
66
 
51
67
  /**
52
68
  * Return a new {@link GeneratorMetadata} with every collection ordered by a
@@ -60,7 +76,7 @@ export function sortGeneratorMetadata(
60
76
  return {
61
77
  version: metadata.version,
62
78
  schemas: [...metadata.schemas].sort(
63
- (a, b) => a.name.localeCompare(b.name) || a.id - b.id,
79
+ (a, b) => compareStrings(a.name, b.name) || a.id - b.id,
64
80
  ),
65
81
  tables: [...metadata.tables].sort(bySchemaName),
66
82
  foreignTables: [...metadata.foreignTables].sort(bySchemaName),
@@ -70,17 +86,17 @@ export function sortGeneratorMetadata(
70
86
  // (matches the TypeScript generator's per-table `columns.sort(by name)`).
71
87
  columns: [...metadata.columns].sort(
72
88
  (a, b) =>
73
- a.schema.localeCompare(b.schema) ||
74
- a.table.localeCompare(b.table) ||
75
- a.name.localeCompare(b.name),
89
+ compareStrings(a.schema, b.schema) ||
90
+ compareStrings(a.table, b.table) ||
91
+ compareStrings(a.name, b.name),
76
92
  ),
77
93
  // Grouped by table like `columns`; within a table the SQL's declared
78
94
  // column order (`array_position(indkey, attnum)`) is preserved by the
79
95
  // stable sort, since composite key order is meaningful.
80
96
  primaryKeys: [...metadata.primaryKeys].sort(
81
97
  (a, b) =>
82
- a.schema.localeCompare(b.schema) ||
83
- a.table_name.localeCompare(b.table_name),
98
+ compareStrings(a.schema, b.schema) ||
99
+ compareStrings(a.table_name, b.table_name),
84
100
  ),
85
101
  relationships: [...metadata.relationships].sort(byRelationship),
86
102
  // Functions can overload, so the signature is part of the identity. Args
@@ -88,14 +104,17 @@ export function sortGeneratorMetadata(
88
104
  functions: [...metadata.functions]
89
105
  .sort(
90
106
  (a, b) =>
91
- a.schema.localeCompare(b.schema) ||
92
- a.name.localeCompare(b.name) ||
93
- a.identity_argument_types.localeCompare(b.identity_argument_types) ||
107
+ compareStrings(a.schema, b.schema) ||
108
+ compareStrings(a.name, b.name) ||
109
+ compareStrings(
110
+ a.identity_argument_types,
111
+ b.identity_argument_types,
112
+ ) ||
94
113
  a.id - b.id,
95
114
  )
96
115
  .map((fn) => ({
97
116
  ...fn,
98
- args: [...fn.args].sort((a, b) => a.name.localeCompare(b.name)),
117
+ args: [...fn.args].sort((a, b) => compareStrings(a.name, b.name)),
99
118
  })),
100
119
  types: [...metadata.types].sort(bySchemaName),
101
120
  };
package/src/types.ts CHANGED
@@ -159,12 +159,14 @@ const postgresViewSchema = type({
159
159
  /**
160
160
  * Whether INSERT works through this view: auto-updatable per
161
161
  * `pg_relation_is_updatable`, or made insertable by an INSTEAD OF INSERT
162
- * trigger. Tracked separately from `is_updatable` (which mirrors
163
- * `information_schema.views` and ignores triggers).
162
+ * trigger or an unconditional INSTEAD rule. Tracked separately from
163
+ * `is_updatable` (which mirrors `information_schema.views` and ignores
164
+ * triggers). Optional because version 1 metadata predating this field is
165
+ * still valid; consumers fall back to `is_updatable` when absent.
164
166
  */
165
- is_insert_enabled: "boolean",
167
+ "is_insert_enabled?": "boolean",
166
168
  /** Same as `is_insert_enabled`, for UPDATE. */
167
- is_update_enabled: "boolean",
169
+ "is_update_enabled?": "boolean",
168
170
  comment: "string | null",
169
171
  "columns?": postgresColumnSchema.array(),
170
172
  });