@supabase/postgrest-typegen 0.2.1 → 0.2.2

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 (56) hide show
  1. package/README.md +8 -0
  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/swift.js +9 -2
  6. package/dist/generation/typescript.d.ts +18 -7
  7. package/dist/generation/typescript.js +102 -79
  8. package/dist/introspection/index.js +6 -23
  9. package/dist/introspection/relationships.d.ts +4 -4
  10. package/dist/introspection/relationships.js +4 -11
  11. package/dist/introspection/sql/columns.sql.d.ts +2 -7
  12. package/dist/introspection/sql/columns.sql.js +23 -17
  13. package/dist/introspection/sql/common.d.ts +7 -15
  14. package/dist/introspection/sql/foreign_tables.sql.d.ts +2 -6
  15. package/dist/introspection/sql/foreign_tables.sql.js +0 -5
  16. package/dist/introspection/sql/functions.sql.d.ts +2 -5
  17. package/dist/introspection/sql/functions.sql.js +0 -33
  18. package/dist/introspection/sql/materialized_views.sql.d.ts +2 -4
  19. package/dist/introspection/sql/materialized_views.sql.js +0 -5
  20. package/dist/introspection/sql/primary_keys.sql.d.ts +2 -4
  21. package/dist/introspection/sql/primary_keys.sql.js +0 -4
  22. package/dist/introspection/sql/schemas.sql.d.ts +2 -6
  23. package/dist/introspection/sql/schemas.sql.js +2 -6
  24. package/dist/introspection/sql/table.sql.d.ts +2 -4
  25. package/dist/introspection/sql/table.sql.js +0 -5
  26. package/dist/introspection/sql/table_relationships.sql.d.ts +2 -2
  27. package/dist/introspection/sql/types.sql.d.ts +5 -5
  28. package/dist/introspection/sql/types.sql.js +6 -17
  29. package/dist/introspection/sql/views.sql.d.ts +2 -4
  30. package/dist/introspection/sql/views.sql.js +0 -5
  31. package/dist/introspection/sql/views_key_dependencies.sql.d.ts +2 -2
  32. package/dist/introspection/sql/views_key_dependencies.sql.js +64 -75
  33. package/dist/sort.js +16 -15
  34. package/dist/types.d.ts +4 -4
  35. package/dist/types.js +6 -4
  36. package/package.json +3 -3
  37. package/src/collation.ts +16 -0
  38. package/src/generation/index.ts +1 -0
  39. package/src/generation/swift.ts +9 -2
  40. package/src/generation/typescript.ts +161 -104
  41. package/src/introspection/index.ts +7 -32
  42. package/src/introspection/relationships.ts +10 -19
  43. package/src/introspection/sql/columns.sql.ts +25 -26
  44. package/src/introspection/sql/common.ts +7 -17
  45. package/src/introspection/sql/foreign_tables.sql.ts +2 -13
  46. package/src/introspection/sql/functions.sql.ts +2 -44
  47. package/src/introspection/sql/materialized_views.sql.ts +2 -11
  48. package/src/introspection/sql/primary_keys.sql.ts +2 -10
  49. package/src/introspection/sql/schemas.sql.ts +4 -14
  50. package/src/introspection/sql/table.sql.ts +2 -11
  51. package/src/introspection/sql/table_relationships.sql.ts +2 -4
  52. package/src/introspection/sql/types.sql.ts +6 -26
  53. package/src/introspection/sql/views.sql.ts +2 -11
  54. package/src/introspection/sql/views_key_dependencies.sql.ts +83 -77
  55. package/src/sort.ts +20 -15
  56. package/src/types.ts +6 -4
@@ -1,14 +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 COLUMNS_SQL = (
5
- props: SQLQueryPropsWithSchemaFilter & {
6
- tableIdFilter?: string;
7
- tableIdentifierFilter?: string;
8
- columnNameFilter?: string;
9
- idsFilter?: string;
10
- },
11
- ) => /* SQL */ `
3
+ export const COLUMNS_SQL = (props: SchemaFilterProps) => /* SQL */ `
12
4
  -- Adapted from information_schema.columns
13
5
 
14
6
  SELECT
@@ -45,30 +37,43 @@ SELECT
45
37
  WHEN 'd' THEN 'BY DEFAULT'
46
38
  ELSE NULL
47
39
  END AS identity_generation,
48
- a.attgenerated IN ('s') AS is_generated,
40
+ -- 's' = stored, 'v' = virtual (PostgreSQL 18+); neither accepts writes.
41
+ a.attgenerated IN ('s', 'v') AS is_generated,
49
42
  NOT (
50
43
  a.attnotnull
51
44
  OR t.typtype = 'd' AND t.typnotnull
52
45
  ) AS is_nullable,
53
46
  (
54
47
  c.relkind IN ('r', 'p')
55
- -- Columns of views made writable by INSTEAD OF triggers can be written
56
- -- through PostgREST even though the view is not auto-updatable, so they
57
- -- must not degrade to \`?: never\` in generated Insert/Update types.
58
- -- include_triggers => TRUE covers INSTEAD OF UPDATE triggers, but
59
- -- pg_column_is_updatable only considers the UPDATE event, so INSTEAD OF
60
- -- INSERT triggers (tgtype bits: 64 = INSTEAD, 4 = INSERT) are checked
61
- -- separately. Which events a view accepts is gated per view via
48
+ -- Columns of views made writable by INSTEAD OF triggers or unconditional
49
+ -- INSTEAD rules can be written through PostgREST even though the view is
50
+ -- not auto-updatable, so they must not degrade to \`?: never\` in generated
51
+ -- Insert/Update types. pg_column_is_updatable cannot express this: it
52
+ -- requires the relation to support both UPDATE and DELETE, so a view with
53
+ -- only an INSTEAD OF INSERT or only an INSTEAD OF UPDATE trigger reports
54
+ -- every column as non-updatable regardless of include_triggers. Triggers
55
+ -- and rules rewrite whole rows, so their presence makes every column
56
+ -- writable (tgtype bits: 64 = INSTEAD, 4 = INSERT, 16 = UPDATE;
57
+ -- pg_rewrite ev_type: '2' = UPDATE, '3' = INSERT; ev_qual is '<>' for
58
+ -- unconditional rules, and only unconditional INSTEAD rules make a view
59
+ -- writable). Which events a view accepts is gated per view via
62
60
  -- is_insert_enabled / is_update_enabled.
63
61
  OR c.relkind IN ('v', 'f') AND (
64
- pg_column_is_updatable(c.oid, a.attnum, TRUE)
62
+ pg_column_is_updatable(c.oid, a.attnum, FALSE)
65
63
  OR EXISTS (
66
64
  SELECT 1 FROM pg_trigger tg
67
65
  WHERE tg.tgrelid = c.oid
68
66
  AND tg.tgtype & 64 <> 0
69
- AND tg.tgtype & 4 <> 0
67
+ AND tg.tgtype & 20 <> 0
70
68
  AND NOT tg.tgisinternal
71
69
  )
70
+ OR EXISTS (
71
+ SELECT 1 FROM pg_rewrite rw
72
+ WHERE rw.ev_class = c.oid
73
+ AND rw.is_instead
74
+ AND rw.ev_type IN ('2', '3')
75
+ AND rw.ev_qual :: text = '<>'
76
+ )
72
77
  )
73
78
  ) AS is_updatable,
74
79
  uniques.table_id IS NOT NULL AS is_unique,
@@ -127,10 +132,6 @@ FROM
127
132
  ) AS check_constraints ON check_constraints.table_id = c.oid AND check_constraints.ordinal_position = a.attnum
128
133
  WHERE
129
134
  ${props.schemaFilter ? `nc.nspname ${props.schemaFilter} AND` : ""}
130
- ${props.idsFilter ? `(c.oid || '.' || a.attnum) ${props.idsFilter} AND` : ""}
131
- ${props.columnNameFilter ? `(c.relname || '.' || a.attname) ${props.columnNameFilter} AND` : ""}
132
- ${props.tableIdFilter ? `c.oid ${props.tableIdFilter} AND` : ""}
133
- ${props.tableIdentifierFilter ? `nc.nspname || '.' || c.relname ${props.tableIdentifierFilter} AND` : ""}
134
135
  NOT pg_is_other_temp_schema(nc.oid)
135
136
  AND a.attnum > 0
136
137
  AND NOT a.attisdropped
@@ -143,6 +144,4 @@ WHERE
143
144
  'SELECT, INSERT, UPDATE, REFERENCES'
144
145
  )
145
146
  )
146
- ${props.limit ? `limit ${literal(props.limit)}` : ""}
147
- ${props.offset ? `offset ${literal(props.offset)}` : ""}
148
147
  `;
@@ -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,8 +34,8 @@ 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
41
  // Mirrors the TypeScript generator's historical `relationships.sort`.
@@ -42,9 +43,10 @@ const byRelationship = (
42
43
  a: PostgresRelationship,
43
44
  b: PostgresRelationship,
44
45
  ): 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(
46
+ compareStrings(a.foreign_key_name, b.foreign_key_name) ||
47
+ compareStrings(a.referenced_relation, b.referenced_relation) ||
48
+ compareStrings(
49
+ JSON.stringify(a.referenced_columns),
48
50
  JSON.stringify(b.referenced_columns),
49
51
  );
50
52
 
@@ -60,7 +62,7 @@ export function sortGeneratorMetadata(
60
62
  return {
61
63
  version: metadata.version,
62
64
  schemas: [...metadata.schemas].sort(
63
- (a, b) => a.name.localeCompare(b.name) || a.id - b.id,
65
+ (a, b) => compareStrings(a.name, b.name) || a.id - b.id,
64
66
  ),
65
67
  tables: [...metadata.tables].sort(bySchemaName),
66
68
  foreignTables: [...metadata.foreignTables].sort(bySchemaName),
@@ -70,17 +72,17 @@ export function sortGeneratorMetadata(
70
72
  // (matches the TypeScript generator's per-table `columns.sort(by name)`).
71
73
  columns: [...metadata.columns].sort(
72
74
  (a, b) =>
73
- a.schema.localeCompare(b.schema) ||
74
- a.table.localeCompare(b.table) ||
75
- a.name.localeCompare(b.name),
75
+ compareStrings(a.schema, b.schema) ||
76
+ compareStrings(a.table, b.table) ||
77
+ compareStrings(a.name, b.name),
76
78
  ),
77
79
  // Grouped by table like `columns`; within a table the SQL's declared
78
80
  // column order (`array_position(indkey, attnum)`) is preserved by the
79
81
  // stable sort, since composite key order is meaningful.
80
82
  primaryKeys: [...metadata.primaryKeys].sort(
81
83
  (a, b) =>
82
- a.schema.localeCompare(b.schema) ||
83
- a.table_name.localeCompare(b.table_name),
84
+ compareStrings(a.schema, b.schema) ||
85
+ compareStrings(a.table_name, b.table_name),
84
86
  ),
85
87
  relationships: [...metadata.relationships].sort(byRelationship),
86
88
  // Functions can overload, so the signature is part of the identity. Args
@@ -88,14 +90,17 @@ export function sortGeneratorMetadata(
88
90
  functions: [...metadata.functions]
89
91
  .sort(
90
92
  (a, b) =>
91
- a.schema.localeCompare(b.schema) ||
92
- a.name.localeCompare(b.name) ||
93
- a.identity_argument_types.localeCompare(b.identity_argument_types) ||
93
+ compareStrings(a.schema, b.schema) ||
94
+ compareStrings(a.name, b.name) ||
95
+ compareStrings(
96
+ a.identity_argument_types,
97
+ b.identity_argument_types,
98
+ ) ||
94
99
  a.id - b.id,
95
100
  )
96
101
  .map((fn) => ({
97
102
  ...fn,
98
- args: [...fn.args].sort((a, b) => a.name.localeCompare(b.name)),
103
+ args: [...fn.args].sort((a, b) => compareStrings(a.name, b.name)),
99
104
  })),
100
105
  types: [...metadata.types].sort(bySchemaName),
101
106
  };