@intentius/chant-lexicon-sql 0.121.1 → 0.123.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/README.md +1 -1
- package/dist/clickhouse/catalog-types.d.ts +8 -0
- package/dist/clickhouse/catalog-types.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +37 -11
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/import/generator.d.ts.map +1 -1
- package/dist/clickhouse/overlays/codecs.d.ts.map +1 -1
- package/dist/clickhouse/overlays/kinds.d.ts +1 -1
- package/dist/clickhouse/overlays/kinds.d.ts.map +1 -1
- package/dist/clickhouse/overlays/types.d.ts.map +1 -1
- package/dist/clickhouse/plan/commands.d.ts.map +1 -1
- package/dist/clickhouse/row-types.d.ts +30 -0
- package/dist/clickhouse/row-types.d.ts.map +1 -0
- package/dist/clickhouse/type-syntax.d.ts +33 -0
- package/dist/clickhouse/type-syntax.d.ts.map +1 -0
- package/dist/clickhouse.d.ts +1 -1
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/clickhouse-module.d.ts.map +1 -1
- package/dist/codegen/postgres-module.d.ts.map +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/typed.d.ts +132 -0
- package/dist/core/typed.d.ts.map +1 -0
- package/dist/generated/clickhouse.d.ts +7 -1
- package/dist/generated/clickhouse.d.ts.map +1 -1
- package/dist/generated/postgres.d.ts +6 -0
- package/dist/generated/postgres.d.ts.map +1 -1
- package/dist/integrity.json +27 -5
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/audit-entries.d.ts.map +1 -1
- package/dist/lint/post-synth/clickhouse-type-check.d.ts +37 -0
- package/dist/lint/post-synth/clickhouse-type-check.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-audit-entries.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-names.d.ts +151 -0
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -0
- package/dist/lint/post-synth/sql101.d.ts +9 -0
- package/dist/lint/post-synth/sql101.d.ts.map +1 -0
- package/dist/lint/post-synth/sql102.d.ts +16 -0
- package/dist/lint/post-synth/sql102.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch121.d.ts +10 -0
- package/dist/lint/post-synth/sqlch121.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch122.d.ts +10 -0
- package/dist/lint/post-synth/sqlch122.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch123.d.ts +9 -0
- package/dist/lint/post-synth/sqlch123.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch124.d.ts +6 -0
- package/dist/lint/post-synth/sqlch124.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch125.d.ts +11 -0
- package/dist/lint/post-synth/sqlch125.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch126.d.ts +12 -0
- package/dist/lint/post-synth/sqlch126.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlch127.d.ts +22 -0
- package/dist/lint/post-synth/sqlch127.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg119.d.ts +18 -0
- package/dist/lint/post-synth/sqlpg119.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg120.d.ts +17 -0
- package/dist/lint/post-synth/sqlpg120.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg121.d.ts +12 -0
- package/dist/lint/post-synth/sqlpg121.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg122.d.ts +12 -0
- package/dist/lint/post-synth/sqlpg122.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg123.d.ts +17 -0
- package/dist/lint/post-synth/sqlpg123.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg124.d.ts +16 -0
- package/dist/lint/post-synth/sqlpg124.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg125.d.ts +17 -0
- package/dist/lint/post-synth/sqlpg125.d.ts.map +1 -0
- package/dist/lint/post-synth/sqlpg126.d.ts +19 -0
- package/dist/lint/post-synth/sqlpg126.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/sqlch004.d.ts +19 -0
- package/dist/lint/rules/sqlch004.d.ts.map +1 -0
- package/dist/lint/rules/sqlpg005.d.ts +18 -0
- package/dist/lint/rules/sqlpg005.d.ts.map +1 -0
- package/dist/lint/rules/typed-names.d.ts +43 -0
- package/dist/lint/rules/typed-names.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +19 -0
- package/dist/okf/rules/SQL101.md +23 -0
- package/dist/okf/rules/SQL102.md +11 -0
- package/dist/okf/rules/SQLCH004.md +11 -0
- package/dist/okf/rules/SQLCH121.md +11 -0
- package/dist/okf/rules/SQLCH122.md +11 -0
- package/dist/okf/rules/SQLCH123.md +11 -0
- package/dist/okf/rules/SQLCH124.md +18 -0
- package/dist/okf/rules/SQLCH125.md +11 -0
- package/dist/okf/rules/SQLCH126.md +15 -0
- package/dist/okf/rules/SQLCH127.md +15 -0
- package/dist/okf/rules/SQLPG005.md +11 -0
- package/dist/okf/rules/SQLPG119.md +16 -0
- package/dist/okf/rules/SQLPG120.md +16 -0
- package/dist/okf/rules/SQLPG121.md +11 -0
- package/dist/okf/rules/SQLPG122.md +15 -0
- package/dist/okf/rules/SQLPG123.md +16 -0
- package/dist/okf/rules/SQLPG124.md +11 -0
- package/dist/okf/rules/SQLPG125.md +11 -0
- package/dist/okf/rules/SQLPG126.md +11 -0
- package/dist/okf/types/Dictionary.md +4 -0
- package/dist/okf/types/Function.md +5 -0
- package/dist/okf/types/Grant.md +6 -0
- package/dist/okf/types/MaterializedView.md +1 -0
- package/dist/okf/types/PostgresDefaultPrivileges.md +4 -0
- package/dist/okf/types/PostgresDomain.md +2 -0
- package/dist/okf/types/PostgresEnum.md +1 -0
- package/dist/okf/types/PostgresFunction.md +4 -0
- package/dist/okf/types/PostgresGrant.md +5 -0
- package/dist/okf/types/PostgresPolicy.md +4 -0
- package/dist/okf/types/PostgresProcedure.md +4 -0
- package/dist/okf/types/PostgresSequence.md +2 -0
- package/dist/okf/types/PostgresTrigger.md +4 -0
- package/dist/okf/types/RowPolicy.md +4 -0
- package/dist/okf/types/Table.md +1 -0
- package/dist/okf/types/View.md +1 -0
- package/dist/postgres/entities.d.ts +38 -9
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/import/generator.d.ts.map +1 -1
- package/dist/postgres/overlays/extension-objects.d.ts +28 -0
- package/dist/postgres/overlays/extension-objects.d.ts.map +1 -0
- package/dist/postgres/overlays/types.d.ts +10 -0
- package/dist/postgres/overlays/types.d.ts.map +1 -1
- package/dist/postgres/row-types.d.ts +17 -0
- package/dist/postgres/row-types.d.ts.map +1 -0
- package/dist/postgres/testing/migration.d.ts +3 -2
- package/dist/postgres/testing/migration.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -1
- package/dist/postgres.d.ts.map +1 -1
- package/dist/rules/audit-entries.ts +7 -0
- package/dist/rules/clickhouse-type-check.ts +173 -0
- package/dist/rules/postgres-audit-entries.ts +8 -0
- package/dist/rules/postgres-names.ts +510 -0
- package/dist/rules/sql101.ts +62 -0
- package/dist/rules/sql102.ts +34 -0
- package/dist/rules/sqlch004.ts +81 -0
- package/dist/rules/sqlch121.ts +12 -0
- package/dist/rules/sqlch122.ts +12 -0
- package/dist/rules/sqlch123.ts +11 -0
- package/dist/rules/sqlch124.ts +21 -0
- package/dist/rules/sqlch125.ts +56 -0
- package/dist/rules/sqlch126.ts +57 -0
- package/dist/rules/sqlch127.ts +128 -0
- package/dist/rules/sqlpg005.ts +73 -0
- package/dist/rules/sqlpg119.ts +37 -0
- package/dist/rules/sqlpg120.ts +70 -0
- package/dist/rules/sqlpg121.ts +32 -0
- package/dist/rules/sqlpg122.ts +28 -0
- package/dist/rules/sqlpg123.ts +63 -0
- package/dist/rules/sqlpg124.ts +47 -0
- package/dist/rules/sqlpg125.ts +40 -0
- package/dist/rules/sqlpg126.ts +37 -0
- package/dist/rules/typed-names.ts +120 -0
- package/dist/sql-types-command.d.ts +19 -0
- package/dist/sql-types-command.d.ts.map +1 -0
- package/dist/sql-types.d.ts +53 -0
- package/dist/sql-types.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/clickhouse/catalog-types.ts +9 -0
- package/src/clickhouse/dictionary.test.ts +1 -1
- package/src/clickhouse/entities.ts +79 -14
- package/src/clickhouse/import/generator.ts +7 -1
- package/src/clickhouse/import/roundtrip.test.ts +1 -1
- package/src/clickhouse/overlays/codecs.ts +6 -2
- package/src/clickhouse/overlays/kinds.ts +1 -1
- package/src/clickhouse/overlays/types.ts +20 -3
- package/src/clickhouse/plan/commands.ts +5 -0
- package/src/clickhouse/row-types.ts +158 -0
- package/src/clickhouse/type-syntax.ts +82 -0
- package/src/clickhouse.ts +10 -0
- package/src/codegen/clickhouse-module.ts +15 -1
- package/src/codegen/postgres-module.ts +43 -0
- package/src/composites/composites.test.ts +7 -6
- package/src/core/index.ts +4 -0
- package/src/core/typed.ts +143 -0
- package/src/generated/clickhouse.ts +1905 -6
- package/src/generated/postgres.ts +2639 -0
- package/src/lint/audit-catalog.ts +28 -0
- package/src/lint/post-synth/audit-entries.ts +7 -0
- package/src/lint/post-synth/clickhouse-catalog-checks.test.ts +315 -0
- package/src/lint/post-synth/clickhouse-type-check.ts +173 -0
- package/src/lint/post-synth/index.ts +34 -0
- package/src/lint/post-synth/postgres-audit-entries.ts +8 -0
- package/src/lint/post-synth/postgres-checks.test.ts +214 -1
- package/src/lint/post-synth/postgres-names.ts +510 -0
- package/src/lint/post-synth/sql101.ts +62 -0
- package/src/lint/post-synth/sql102.ts +34 -0
- package/src/lint/post-synth/sqlch121.ts +12 -0
- package/src/lint/post-synth/sqlch122.ts +12 -0
- package/src/lint/post-synth/sqlch123.ts +11 -0
- package/src/lint/post-synth/sqlch124.ts +21 -0
- package/src/lint/post-synth/sqlch125.ts +56 -0
- package/src/lint/post-synth/sqlch126.ts +57 -0
- package/src/lint/post-synth/sqlch127.ts +128 -0
- package/src/lint/post-synth/sqlpg119.ts +37 -0
- package/src/lint/post-synth/sqlpg120.ts +70 -0
- package/src/lint/post-synth/sqlpg121.ts +32 -0
- package/src/lint/post-synth/sqlpg122.ts +28 -0
- package/src/lint/post-synth/sqlpg123.ts +63 -0
- package/src/lint/post-synth/sqlpg124.ts +47 -0
- package/src/lint/post-synth/sqlpg125.ts +40 -0
- package/src/lint/post-synth/sqlpg126.ts +37 -0
- package/src/lint/rules/index.ts +4 -2
- package/src/lint/rules/sqlch004.ts +81 -0
- package/src/lint/rules/sqlpg005.ts +73 -0
- package/src/lint/rules/typed-names.test.ts +151 -0
- package/src/lint/rules/typed-names.ts +120 -0
- package/src/op/__snapshots__/watch-pipeline.test.ts.snap +3 -3
- package/src/postgres/entities.ts +65 -12
- package/src/postgres/import/generator.ts +7 -1
- package/src/postgres/overlays/extension-objects.ts +100 -0
- package/src/postgres/overlays/types.ts +25 -0
- package/src/postgres/row-types.ts +111 -0
- package/src/postgres.ts +10 -0
- package/src/sql-types-command.ts +99 -0
- package/src/sql-types-staleness.test.ts +110 -0
- package/src/sql-types.test.ts +132 -0
- package/src/sql-types.ts +352 -0
- package/src/typecheck.test.ts +12 -3
- package/src/typed-columns.test.ts +92 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the common extensions add, for lint: the types, functions, index
|
|
3
|
+
* access methods and operator classes a declaration may name once the project
|
|
4
|
+
* declares the extension. The pinned catalogs are read from a server with no
|
|
5
|
+
* extension installed, so none of these are in them.
|
|
6
|
+
*
|
|
7
|
+
* The list is not exhaustive. A name the core catalog does not have and no
|
|
8
|
+
* entry here covers is reported as a warning when the project declares any
|
|
9
|
+
* extension (it may come from one), and as an error when it declares none.
|
|
10
|
+
* A name an entry here covers, used without that extension declared, is an
|
|
11
|
+
* error that names the extension.
|
|
12
|
+
*
|
|
13
|
+
* Sources: the reference page of each contrib module
|
|
14
|
+
* (https://www.postgresql.org/docs/18/contrib.html), PostGIS
|
|
15
|
+
* (https://postgis.net/docs/reference.html) and pgvector
|
|
16
|
+
* (https://github.com/pgvector/pgvector#reference).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface ExtensionObjects {
|
|
20
|
+
types?: readonly string[];
|
|
21
|
+
functions?: readonly string[];
|
|
22
|
+
/** Function name prefixes the extension owns (`st_` for PostGIS). */
|
|
23
|
+
functionPrefixes?: readonly string[];
|
|
24
|
+
accessMethods?: readonly string[];
|
|
25
|
+
/** Operator classes by access method. */
|
|
26
|
+
opclasses?: Readonly<Record<string, readonly string[]>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const BTREE_GIN = [
|
|
30
|
+
"int2_ops", "int4_ops", "int8_ops", "float4_ops", "float8_ops", "money_ops", "oid_ops", "timestamp_ops", "timestamptz_ops",
|
|
31
|
+
"time_ops", "timetz_ops", "date_ops", "interval_ops", "macaddr_ops", "macaddr8_ops", "inet_ops", "cidr_ops", "text_ops",
|
|
32
|
+
"varchar_ops", "char_ops", "bytea_ops", "bit_ops", "varbit_ops", "numeric_ops", "enum_ops", "uuid_ops", "name_ops",
|
|
33
|
+
"bool_ops", "bpchar_ops",
|
|
34
|
+
];
|
|
35
|
+
const BTREE_GIST = [
|
|
36
|
+
"gist_int2_ops", "gist_int4_ops", "gist_int8_ops", "gist_float4_ops", "gist_float8_ops", "gist_numeric_ops", "gist_oid_ops",
|
|
37
|
+
"gist_timestamp_ops", "gist_timestamptz_ops", "gist_time_ops", "gist_timetz_ops", "gist_date_ops", "gist_interval_ops",
|
|
38
|
+
"gist_cash_ops", "gist_macaddr_ops", "gist_macaddr8_ops", "gist_text_ops", "gist_bpchar_ops", "gist_bytea_ops", "gist_bit_ops",
|
|
39
|
+
"gist_vbit_ops", "gist_inet_ops", "gist_cidr_ops", "gist_uuid_ops", "gist_enum_ops", "gist_bool_ops",
|
|
40
|
+
];
|
|
41
|
+
const VECTOR_OPS = ["vector_l2_ops", "vector_ip_ops", "vector_cosine_ops", "vector_l1_ops", "halfvec_l2_ops", "halfvec_ip_ops", "halfvec_cosine_ops", "halfvec_l1_ops", "sparsevec_l2_ops", "sparsevec_ip_ops", "sparsevec_cosine_ops", "sparsevec_l1_ops", "bit_hamming_ops", "bit_jaccard_ops"];
|
|
42
|
+
|
|
43
|
+
export const EXTENSION_OBJECTS: Readonly<Record<string, ExtensionObjects>> = {
|
|
44
|
+
bloom: { accessMethods: ["bloom"], opclasses: { bloom: ["int4_ops", "text_ops"] } },
|
|
45
|
+
btree_gin: { opclasses: { gin: BTREE_GIN } },
|
|
46
|
+
btree_gist: { opclasses: { gist: BTREE_GIST } },
|
|
47
|
+
citext: { types: ["citext"], opclasses: { btree: ["citext_ops", "citext_pattern_ops"], hash: ["citext_ops"] } },
|
|
48
|
+
cube: { types: ["cube"], functions: ["cube", "cube_dim", "cube_ll_coord", "cube_ur_coord", "cube_is_point", "cube_enlarge", "cube_subset", "cube_union", "cube_inter", "cube_size", "cube_distance"], opclasses: { gist: ["gist_cube_ops"] } },
|
|
49
|
+
earthdistance: { types: ["earth"], functions: ["earth", "earth_distance", "earth_box", "ll_to_earth", "latitude", "longitude", "sec_to_gc", "gc_to_sec"] },
|
|
50
|
+
fuzzystrmatch: { functions: ["soundex", "difference", "levenshtein", "levenshtein_less_equal", "metaphone", "dmetaphone", "dmetaphone_alt", "daitch_mokotoff"] },
|
|
51
|
+
hstore: {
|
|
52
|
+
types: ["hstore"],
|
|
53
|
+
functions: ["hstore", "akeys", "avals", "skeys", "svals", "hstore_to_array", "hstore_to_matrix", "hstore_to_json", "hstore_to_jsonb", "hstore_to_json_loose", "hstore_to_jsonb_loose", "slice", "exist", "defined", "delete", "each", "populate_record"],
|
|
54
|
+
opclasses: { gist: ["gist_hstore_ops"], gin: ["gin_hstore_ops"], btree: ["btree_hstore_ops"], hash: ["hash_hstore_ops"] },
|
|
55
|
+
},
|
|
56
|
+
intarray: {
|
|
57
|
+
functions: ["icount", "sort", "sort_asc", "sort_desc", "uniq", "idx", "subarray", "intset"],
|
|
58
|
+
opclasses: { gist: ["gist__int_ops", "gist__intbig_ops"], gin: ["gin__int_ops"] },
|
|
59
|
+
},
|
|
60
|
+
isn: { types: ["ean13", "isbn", "isbn13", "ismn", "ismn13", "issn", "issn13", "upc"], functions: ["isn_weak", "make_valid", "is_valid"] },
|
|
61
|
+
ltree: {
|
|
62
|
+
types: ["ltree", "lquery", "ltxtquery"],
|
|
63
|
+
functions: ["subltree", "subpath", "nlevel", "index", "text2ltree", "ltree2text", "lca"],
|
|
64
|
+
opclasses: { gist: ["gist_ltree_ops", "gist__ltree_ops"], btree: ["ltree_ops"], hash: ["hash_ltree_ops"] },
|
|
65
|
+
},
|
|
66
|
+
pg_trgm: {
|
|
67
|
+
functions: ["similarity", "word_similarity", "strict_word_similarity", "show_trgm", "show_limit", "set_limit"],
|
|
68
|
+
opclasses: { gin: ["gin_trgm_ops"], gist: ["gist_trgm_ops"] },
|
|
69
|
+
},
|
|
70
|
+
pgcrypto: {
|
|
71
|
+
functions: [
|
|
72
|
+
"digest", "hmac", "crypt", "gen_salt", "gen_random_bytes", "gen_random_uuid", "encrypt", "decrypt", "encrypt_iv", "decrypt_iv",
|
|
73
|
+
"pgp_sym_encrypt", "pgp_sym_encrypt_bytea", "pgp_sym_decrypt", "pgp_sym_decrypt_bytea", "pgp_pub_encrypt", "pgp_pub_encrypt_bytea",
|
|
74
|
+
"pgp_pub_decrypt", "pgp_pub_decrypt_bytea", "pgp_key_id", "armor", "dearmor", "pgp_armor_headers",
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
postgis: {
|
|
78
|
+
types: ["geometry", "geography", "box2d", "box3d"],
|
|
79
|
+
functionPrefixes: ["st_", "postgis_"],
|
|
80
|
+
opclasses: {
|
|
81
|
+
gist: ["gist_geometry_ops_2d", "gist_geometry_ops_nd", "gist_geography_ops"],
|
|
82
|
+
brin: ["brin_geometry_inclusion_ops_2d", "brin_geometry_inclusion_ops_3d", "brin_geometry_inclusion_ops_4d", "brin_geography_inclusion_ops"],
|
|
83
|
+
spgist: ["spgist_geometry_ops_2d", "spgist_geometry_ops_3d", "spgist_geometry_ops_nd", "spgist_geography_ops_nd"],
|
|
84
|
+
btree: ["btree_geometry_ops", "btree_geography_ops"],
|
|
85
|
+
hash: ["hash_geometry_ops"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
seg: { types: ["seg"], opclasses: { gist: ["gist_seg_ops"] } },
|
|
89
|
+
tablefunc: { functions: ["crosstab", "normal_rand", "connectby"] },
|
|
90
|
+
unaccent: { functions: ["unaccent"] },
|
|
91
|
+
"uuid-ossp": {
|
|
92
|
+
functions: ["uuid_generate_v1", "uuid_generate_v1mc", "uuid_generate_v3", "uuid_generate_v4", "uuid_generate_v5", "uuid_nil", "uuid_ns_dns", "uuid_ns_url", "uuid_ns_oid", "uuid_ns_x500"],
|
|
93
|
+
},
|
|
94
|
+
vector: {
|
|
95
|
+
types: ["vector", "halfvec", "sparsevec"],
|
|
96
|
+
functions: ["l2_distance", "inner_product", "cosine_distance", "l1_distance", "vector_dims", "vector_norm", "l2_norm", "l2_normalize", "binary_quantize", "subvector", "hamming_distance", "jaccard_distance"],
|
|
97
|
+
accessMethods: ["hnsw", "ivfflat"],
|
|
98
|
+
opclasses: { hnsw: VECTOR_OPS, ivfflat: VECTOR_OPS.filter((o) => !o.startsWith("sparsevec") && !o.endsWith("l1_ops") && o !== "bit_jaccard_ops") },
|
|
99
|
+
},
|
|
100
|
+
};
|
|
@@ -54,3 +54,28 @@ export const SERIAL_TYPES: Record<string, string> = {
|
|
|
54
54
|
|
|
55
55
|
/** An array suffix: `integer[]`, `text[3]`, `ARRAY`. Dimensions are documentation only to the server. */
|
|
56
56
|
export const ARRAY_SUFFIX = { brackets: "[]", dimensioned: "[n]", keyword: "ARRAY" } as const;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Spellings the grammar accepts that neither `pg_type` nor {@link TYPE_SPELLINGS}
|
|
60
|
+
* lists, and the canonical name each stands for (gram.y, `Numeric`,
|
|
61
|
+
* `Character`, `ConstDatetime`). Lint reads them beside the catalog so every
|
|
62
|
+
* spelling Postgres accepts resolves. `float(p)` is `real` for p up to 24 and
|
|
63
|
+
* `double precision` above; lint reads the precision.
|
|
64
|
+
*/
|
|
65
|
+
export const GRAMMAR_TYPE_SPELLINGS: Record<string, string> = {
|
|
66
|
+
dec: "numeric",
|
|
67
|
+
"char varying": "character varying",
|
|
68
|
+
"national character": "character",
|
|
69
|
+
"national char": "character",
|
|
70
|
+
nchar: "character",
|
|
71
|
+
"national character varying": "character varying",
|
|
72
|
+
"national char varying": "character varying",
|
|
73
|
+
"nchar varying": "character varying",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** The serial spellings with a width suffix, beside {@link SERIAL_TYPES}. */
|
|
77
|
+
export const SERIAL_WIDTH_SPELLINGS: Record<string, string> = {
|
|
78
|
+
serial2: "smallint",
|
|
79
|
+
serial4: "integer",
|
|
80
|
+
serial8: "bigint",
|
|
81
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Postgres column's value is in TypeScript, for `RowOf` (#3751): the
|
|
3
|
+
* value node-postgres (`pg`, the driver the lexicon itself uses) hands back
|
|
4
|
+
* with its default type parsers. `bigint` and `numeric` are strings, since a
|
|
5
|
+
* JavaScript number cannot hold every value; `integer`, `smallint`, `real`
|
|
6
|
+
* and `double precision` are numbers; dates and timestamps are `Date`s;
|
|
7
|
+
* `json` and `jsonb` are parsed, so their shape is the application's
|
|
8
|
+
* (`unknown`); everything the driver does not parse is its text form.
|
|
9
|
+
*
|
|
10
|
+
* The table names every built-in type of every supported major's catalog
|
|
11
|
+
* (`COLUMN_TYPES`), and a test fails on one it does not, so a type a new
|
|
12
|
+
* catalog adds is a decision rather than a silent `unknown`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { TypeSqlName } from "../generated/postgres-types";
|
|
16
|
+
|
|
17
|
+
const NUMBER = "number";
|
|
18
|
+
const STRING = "string";
|
|
19
|
+
const DATE = "Date";
|
|
20
|
+
|
|
21
|
+
/** The TypeScript type of each built-in type's value, by its `format_type()` name. */
|
|
22
|
+
export const POSTGRES_ROW_TYPES: Readonly<Record<TypeSqlName, string>> = {
|
|
23
|
+
// Numbers the driver parses; the 64-bit and arbitrary-precision ones stay text.
|
|
24
|
+
smallint: NUMBER,
|
|
25
|
+
integer: NUMBER,
|
|
26
|
+
real: NUMBER,
|
|
27
|
+
"double precision": NUMBER,
|
|
28
|
+
oid: NUMBER,
|
|
29
|
+
bigint: STRING,
|
|
30
|
+
numeric: STRING,
|
|
31
|
+
money: STRING,
|
|
32
|
+
regclass: STRING,
|
|
33
|
+
regcollation: STRING,
|
|
34
|
+
regconfig: STRING,
|
|
35
|
+
regdictionary: STRING,
|
|
36
|
+
regnamespace: STRING,
|
|
37
|
+
regoper: STRING,
|
|
38
|
+
regoperator: STRING,
|
|
39
|
+
regproc: STRING,
|
|
40
|
+
regprocedure: STRING,
|
|
41
|
+
regrole: STRING,
|
|
42
|
+
regtype: STRING,
|
|
43
|
+
boolean: "boolean",
|
|
44
|
+
// Strings.
|
|
45
|
+
text: STRING,
|
|
46
|
+
"character varying": STRING,
|
|
47
|
+
character: STRING,
|
|
48
|
+
name: STRING,
|
|
49
|
+
'"char"': STRING,
|
|
50
|
+
uuid: STRING,
|
|
51
|
+
xml: STRING,
|
|
52
|
+
bit: STRING,
|
|
53
|
+
"bit varying": STRING,
|
|
54
|
+
cidr: STRING,
|
|
55
|
+
inet: STRING,
|
|
56
|
+
macaddr: STRING,
|
|
57
|
+
macaddr8: STRING,
|
|
58
|
+
tsvector: STRING,
|
|
59
|
+
tsquery: STRING,
|
|
60
|
+
jsonpath: STRING,
|
|
61
|
+
pg_lsn: STRING,
|
|
62
|
+
pg_snapshot: STRING,
|
|
63
|
+
txid_snapshot: STRING,
|
|
64
|
+
refcursor: STRING,
|
|
65
|
+
aclitem: STRING,
|
|
66
|
+
cid: STRING,
|
|
67
|
+
tid: STRING,
|
|
68
|
+
xid: STRING,
|
|
69
|
+
xid8: STRING,
|
|
70
|
+
int2vector: STRING,
|
|
71
|
+
oidvector: STRING,
|
|
72
|
+
gtsvector: STRING,
|
|
73
|
+
pg_brin_bloom_summary: STRING,
|
|
74
|
+
pg_brin_minmax_multi_summary: STRING,
|
|
75
|
+
pg_dependencies: STRING,
|
|
76
|
+
pg_mcv_list: STRING,
|
|
77
|
+
pg_ndistinct: STRING,
|
|
78
|
+
pg_node_tree: STRING,
|
|
79
|
+
// Dates and times: the driver parses dates and timestamps, not times of day.
|
|
80
|
+
date: DATE,
|
|
81
|
+
"timestamp without time zone": DATE,
|
|
82
|
+
"timestamp with time zone": DATE,
|
|
83
|
+
"time without time zone": STRING,
|
|
84
|
+
"time with time zone": STRING,
|
|
85
|
+
interval: "{ years?: number; months?: number; days?: number; hours?: number; minutes?: number; seconds?: number; milliseconds?: number }",
|
|
86
|
+
// Ranges and multiranges arrive as their text form.
|
|
87
|
+
int4range: STRING,
|
|
88
|
+
int8range: STRING,
|
|
89
|
+
numrange: STRING,
|
|
90
|
+
tsrange: STRING,
|
|
91
|
+
tstzrange: STRING,
|
|
92
|
+
daterange: STRING,
|
|
93
|
+
int4multirange: STRING,
|
|
94
|
+
int8multirange: STRING,
|
|
95
|
+
nummultirange: STRING,
|
|
96
|
+
tsmultirange: STRING,
|
|
97
|
+
tstzmultirange: STRING,
|
|
98
|
+
datemultirange: STRING,
|
|
99
|
+
// Geometry: the driver parses a point and a circle.
|
|
100
|
+
point: "{ x: number; y: number }",
|
|
101
|
+
circle: "{ x: number; y: number; radius: number }",
|
|
102
|
+
line: STRING,
|
|
103
|
+
lseg: STRING,
|
|
104
|
+
box: STRING,
|
|
105
|
+
path: STRING,
|
|
106
|
+
polygon: STRING,
|
|
107
|
+
// Parsed, so the application's own shape.
|
|
108
|
+
json: "unknown",
|
|
109
|
+
jsonb: "unknown",
|
|
110
|
+
bytea: "Uint8Array",
|
|
111
|
+
};
|
package/src/postgres.ts
CHANGED
|
@@ -69,6 +69,16 @@ export {
|
|
|
69
69
|
type DefaultPrivilegesProps,
|
|
70
70
|
type PrivilegeDef,
|
|
71
71
|
type LineageEdge,
|
|
72
|
+
type SqlSchema,
|
|
73
|
+
type RowOf,
|
|
74
|
+
type Column,
|
|
75
|
+
type ColumnTraits,
|
|
76
|
+
type AnyColumn,
|
|
77
|
+
type UncheckedColumn,
|
|
78
|
+
type KeyColumn,
|
|
79
|
+
type NotNullColumn,
|
|
80
|
+
type Named,
|
|
81
|
+
type Unnamed,
|
|
72
82
|
} from "./postgres/entities";
|
|
73
83
|
|
|
74
84
|
export {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant sql types [--output <path>] [--check]` (#3751): build the project the way
|
|
3
|
+
* `chant build` does, folded, and write the declarations
|
|
4
|
+
* {@link renderSqlTypes} renders from its entities, so a tag given its
|
|
5
|
+
* qualified name as the type argument has typed columns.
|
|
6
|
+
*
|
|
7
|
+
* The file goes to `<sourceDir>/sql-types.d.ts` (the project root when
|
|
8
|
+
* `sourceDir` is unset), where any tsconfig that includes the schema includes
|
|
9
|
+
* it too. `--output` puts it elsewhere, as a workspace `generated` entry's
|
|
10
|
+
* generator does. The command refuses to write beside a `sql-types.ts`: tsc
|
|
11
|
+
* resolves the `.ts` first and would drop the declarations. `chant build`
|
|
12
|
+
* never writes the file. `--check` renders the same text and compares it with
|
|
13
|
+
* the file instead of writing it: exit 1, naming the file, when they differ.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
18
|
+
import type { CommandGroupContext } from "@intentius/chant/cli/command-group";
|
|
19
|
+
import { renderSqlTypes, SQL_TYPES_FILE } from "./sql-types";
|
|
20
|
+
|
|
21
|
+
const USAGE = "usage: chant sql types [--output <path>] [--check]";
|
|
22
|
+
|
|
23
|
+
/** The `--output` value, or an error message. */
|
|
24
|
+
function parseArgs(args: readonly string[]): { output?: string; check: boolean } | { error: string } {
|
|
25
|
+
let output: string | undefined;
|
|
26
|
+
let check = false;
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
const a = args[i]!;
|
|
29
|
+
if (a === "--output") {
|
|
30
|
+
output = args[++i];
|
|
31
|
+
if (!output) return { error: "--output needs a path" };
|
|
32
|
+
} else if (a === "--check") check = true;
|
|
33
|
+
else if (a.startsWith("--output=")) output = a.slice("--output=".length);
|
|
34
|
+
else return { error: `unknown argument ${a}` };
|
|
35
|
+
}
|
|
36
|
+
return output === undefined ? { check } : { output, check };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The file tsc would read instead of `path`: `sql-types.ts` beside `sql-types.d.ts`. */
|
|
40
|
+
export function shadowingSource(path: string): string | undefined {
|
|
41
|
+
if (!path.endsWith(".d.ts")) return undefined;
|
|
42
|
+
const source = `${path.slice(0, -".d.ts".length)}.ts`;
|
|
43
|
+
return existsSync(source) ? source : undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runTypes(ctx: CommandGroupContext): Promise<number> {
|
|
47
|
+
const args = parseArgs(ctx.rawArgs);
|
|
48
|
+
if ("error" in args) {
|
|
49
|
+
console.error(`${args.error}\n${USAGE}`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const cwd = process.cwd();
|
|
53
|
+
const { loadChantConfigUpward } = await import("@intentius/chant/config");
|
|
54
|
+
const { config, configPath } = await loadChantConfigUpward(cwd);
|
|
55
|
+
const projectDir = configPath ? dirname(configPath) : cwd;
|
|
56
|
+
const sourceDir = resolve(projectDir, config.sourceDir ?? ".");
|
|
57
|
+
const output = args.output !== undefined ? resolve(cwd, args.output) : join(sourceDir, SQL_TYPES_FILE);
|
|
58
|
+
const shadow = shadowingSource(output);
|
|
59
|
+
if (shadow) {
|
|
60
|
+
console.error(`chant sql types: ${relative(cwd, shadow) || basename(shadow)} sits beside ${relative(cwd, output) || basename(output)}, and tsc reads it instead of the declarations; rename one`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { build } = await import("@intentius/chant/build");
|
|
65
|
+
const { resolveBuildModes, resolveProjectBuildOptions } = await import("@intentius/chant/cli/build-options");
|
|
66
|
+
const { loadPlugins, resolveProjectLexicons } = await import("@intentius/chant/cli/plugins");
|
|
67
|
+
const { sqlSerializer } = await import("./serializer");
|
|
68
|
+
const plugins = await loadPlugins(await resolveProjectLexicons(projectDir, { lexicon: "sql" }));
|
|
69
|
+
// The fold build's entities: what `chant build` declares, `.sql` files and composite members included.
|
|
70
|
+
const options = resolveProjectBuildOptions({ config, configDir: projectDir, plugins, modes: resolveBuildModes(config, { fold: true }) });
|
|
71
|
+
const result = await build(sourceDir, [sqlSerializer], undefined, options);
|
|
72
|
+
if (result.errors.length > 0) {
|
|
73
|
+
for (const e of result.errors) console.error(e instanceof Error ? e.message : String(e));
|
|
74
|
+
console.error("chant sql types: the build failed, so nothing was written");
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
const text = renderSqlTypes(result.entities, { projectDir, config: config as unknown as Record<string, unknown> });
|
|
78
|
+
const shown = relative(cwd, output);
|
|
79
|
+
const named = shown && !shown.startsWith("..") ? shown : output;
|
|
80
|
+
if (args.check) {
|
|
81
|
+
// Compares only; the file is never written.
|
|
82
|
+
const onDisk = existsSync(output) ? readFileSync(output, "utf8") : undefined;
|
|
83
|
+
if (onDisk === text) {
|
|
84
|
+
console.log(`${named} is current`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
console.error(
|
|
88
|
+
onDisk === undefined
|
|
89
|
+
? `chant sql types: ${named} does not exist; run \`chant sql types\` to write it`
|
|
90
|
+
: `chant sql types: ${named} differs from what the build declares; run \`chant sql types\` to rewrite it`,
|
|
91
|
+
);
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
95
|
+
writeFileSync(output, text);
|
|
96
|
+
const count = (text.match(/^ {4}"/gm) ?? []).length;
|
|
97
|
+
console.log(`wrote ${named}: ${count} relation${count === 1 ? "" : "s"}`);
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeping `sql-types.d.ts` current (#3751): `chant sql types --check` and the
|
|
3
|
+
* SQL102 post-synth check, over a scratch project. The project imports the
|
|
4
|
+
* workspace's sql lexicon through a `node_modules` link, as an installed
|
|
5
|
+
* project would.
|
|
6
|
+
*/
|
|
7
|
+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, symlinkSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
12
|
+
import { lintCommand } from "@intentius/chant/cli/commands/lint";
|
|
13
|
+
import { runTypes } from "./sql-types-command";
|
|
14
|
+
import { sqlTypesPath } from "./sql-types";
|
|
15
|
+
import { sql102 } from "./lint/post-synth/sql102";
|
|
16
|
+
|
|
17
|
+
const repoNodeModules = resolve(fileURLToPath(new URL(".", import.meta.url)), "../../../node_modules");
|
|
18
|
+
|
|
19
|
+
const schema = (amount: string) =>
|
|
20
|
+
'import { schema, table } from "@intentius/chant-lexicon-sql/postgres";\n' +
|
|
21
|
+
"export const shop = schema`CREATE SCHEMA shop`;\n" +
|
|
22
|
+
'export const orders = table<"shop.orders">`CREATE TABLE ${shop}.orders (id bigint PRIMARY KEY, ' +
|
|
23
|
+
`amount ${amount})\`;\n`;
|
|
24
|
+
|
|
25
|
+
describe("sql-types.d.ts staleness", () => {
|
|
26
|
+
let root: string;
|
|
27
|
+
let cwd: string;
|
|
28
|
+
let errors: string[];
|
|
29
|
+
|
|
30
|
+
const write = (amount: string) => writeFileSync(join(root, "src", "schema.ts"), schema(amount));
|
|
31
|
+
const types = async (...args: string[]) => {
|
|
32
|
+
process.chdir(root);
|
|
33
|
+
return runTypes({ verb: "types", rawArgs: args });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
cwd = process.cwd();
|
|
38
|
+
root = realpathSync(mkdtempSync(join(tmpdir(), "chant-3751-stale-")));
|
|
39
|
+
mkdirSync(join(root, "src"), { recursive: true });
|
|
40
|
+
symlinkSync(repoNodeModules, join(root, "node_modules"), "dir");
|
|
41
|
+
writeFileSync(join(root, "chant.config.json"), JSON.stringify({ lexicons: ["sql"], sourceDir: "src", sql: { dialect: "postgres" } }));
|
|
42
|
+
write("numeric(12,2)");
|
|
43
|
+
errors = [];
|
|
44
|
+
vi.spyOn(console, "error").mockImplementation((m: unknown) => void errors.push(String(m)));
|
|
45
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
46
|
+
process.env.NO_COLOR = "1";
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
process.chdir(cwd);
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
delete process.env.NO_COLOR;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("--check exits 0 on a current file, and 1 naming it once the schema changes", async () => {
|
|
56
|
+
expect(await types()).toBe(0);
|
|
57
|
+
const file = join(root, "src", "sql-types.d.ts");
|
|
58
|
+
const written = readFileSync(file, "utf8");
|
|
59
|
+
expect(await types("--check")).toBe(0);
|
|
60
|
+
|
|
61
|
+
write("bigint");
|
|
62
|
+
expect(await types("--check")).toBe(1);
|
|
63
|
+
expect(errors.join("\n")).toContain("src/sql-types.d.ts differs");
|
|
64
|
+
expect(readFileSync(file, "utf8")).toBe(written);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("--check on a missing file exits 1 and writes nothing", async () => {
|
|
68
|
+
expect(await types("--check")).toBe(1);
|
|
69
|
+
expect(errors.join("\n")).toContain("src/sql-types.d.ts does not exist");
|
|
70
|
+
expect(() => readFileSync(join(root, "src", "sql-types.d.ts"))).toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("--check honours --output", async () => {
|
|
74
|
+
expect(await types("--output", "out/types.d.ts")).toBe(0);
|
|
75
|
+
expect(await types("--check", "--output", "out/types.d.ts")).toBe(0);
|
|
76
|
+
write("bigint");
|
|
77
|
+
expect(await types("--check", "--output", "out/types.d.ts")).toBe(1);
|
|
78
|
+
expect(errors.join("\n")).toContain("out/types.d.ts");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("lint reports SQL102 for a stale file, and nothing for a current or missing one", async () => {
|
|
82
|
+
const ids = async () => (await lintCommand({ path: root, format: "stylish" })).diagnostics.map((d) => d.ruleId);
|
|
83
|
+
expect(await ids()).not.toContain("SQL102");
|
|
84
|
+
|
|
85
|
+
expect(await types()).toBe(0);
|
|
86
|
+
expect(await ids()).not.toContain("SQL102");
|
|
87
|
+
|
|
88
|
+
write("bigint");
|
|
89
|
+
const result = await lintCommand({ path: root, format: "stylish" });
|
|
90
|
+
const diag = result.diagnostics.find((d) => d.ruleId === "SQL102");
|
|
91
|
+
expect(diag, result.output).toBeDefined();
|
|
92
|
+
expect(diag!.severity).toBe("error");
|
|
93
|
+
expect(diag!.message).toContain("sql-types.d.ts");
|
|
94
|
+
|
|
95
|
+
expect(await types()).toBe(0);
|
|
96
|
+
expect(await ids()).not.toContain("SQL102");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("SQL102 on a hand-built context", () => {
|
|
101
|
+
test("is silent without a project directory", () => {
|
|
102
|
+
const ctx = { outputs: new Map(), entities: new Map(), buildResult: { outputs: new Map(), entities: new Map(), warnings: [], errors: [], sourceFileCount: 0 } };
|
|
103
|
+
expect(sql102.check(ctx)).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("the default path is the source directory, else the project root", () => {
|
|
107
|
+
expect(sqlTypesPath("/p", { sourceDir: "src" })).toBe("/p/src/sql-types.d.ts");
|
|
108
|
+
expect(sqlTypesPath("/p", {})).toBe("/p/sql-types.d.ts");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { describe, expect, test } from "vitest";
|
|
5
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
6
|
+
import { COLUMN_TYPES } from "./generated/postgres";
|
|
7
|
+
import { TYPE_FAMILIES } from "./generated/clickhouse";
|
|
8
|
+
import { POSTGRES_ROW_TYPES } from "./postgres/row-types";
|
|
9
|
+
import { CLICKHOUSE_ROW_TYPES, clickhouseRowType } from "./clickhouse/row-types";
|
|
10
|
+
import * as pg from "./postgres/entities";
|
|
11
|
+
import * as ch from "./clickhouse/entities";
|
|
12
|
+
import { getTypedSlots, relationKey } from "./core/typed";
|
|
13
|
+
import { sqlSerializer } from "./serializer";
|
|
14
|
+
import { renderSqlTypes } from "./sql-types";
|
|
15
|
+
import { runTypes, shadowingSource } from "./sql-types-command";
|
|
16
|
+
|
|
17
|
+
const entities = (o: Record<string, unknown>) => new Map(Object.entries(o)) as Map<string, Declarable>;
|
|
18
|
+
|
|
19
|
+
describe("the row type mapping covers the pinned catalogs", () => {
|
|
20
|
+
test("every Postgres built-in type of every supported major has a row type, and nothing else does", () => {
|
|
21
|
+
expect(Object.keys(POSTGRES_ROW_TYPES).sort()).toEqual(Object.keys(COLUMN_TYPES).sort());
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("every ClickHouse type family has a row type, and nothing else does", () => {
|
|
25
|
+
const families = [...new Set(Object.values(TYPE_FAMILIES).map((f) => f.canonical))].sort();
|
|
26
|
+
expect(Object.keys(CLICKHOUSE_ROW_TYPES).sort()).toEqual(families);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("ClickHouse wrappers and containers compose; an alias reads as its family", () => {
|
|
30
|
+
expect(clickhouseRowType("Nullable(UInt64)")).toEqual({ ts: "string", nullable: true });
|
|
31
|
+
expect(clickhouseRowType("LowCardinality(Nullable(String))")).toEqual({ ts: "string", nullable: true });
|
|
32
|
+
expect(clickhouseRowType("Array(Nullable(Int32))").ts).toBe("(number | null)[]");
|
|
33
|
+
expect(clickhouseRowType("Map(String, Array(UInt8))").ts).toBe("Record<string, number[]>");
|
|
34
|
+
expect(clickhouseRowType("Tuple(String, UInt8)").ts).toBe("[string, number]");
|
|
35
|
+
expect(clickhouseRowType("Enum8('a' = 1, 'it''s' = 2)").ts).toBe('"a" | "it\'s"');
|
|
36
|
+
expect(clickhouseRowType("BIGINT UNSIGNED").ts).toBe("string");
|
|
37
|
+
expect(clickhouseRowType("SimpleAggregateFunction(sum, Float64)").ts).toBe("number");
|
|
38
|
+
expect(clickhouseRowType("NoSuchType").ts).toBe("unknown");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("the slots a tag records", () => {
|
|
43
|
+
test("a Postgres foreign key's interpolated referenced column, paired with the referencing column, off the output", () => {
|
|
44
|
+
const s = pg.schema`CREATE SCHEMA shop`;
|
|
45
|
+
const a = pg.table`CREATE TABLE ${s}.a (id bigint PRIMARY KEY, code text UNIQUE)`;
|
|
46
|
+
const b = pg.table<"shop.b">`
|
|
47
|
+
CREATE TABLE ${s}.b (
|
|
48
|
+
a_id bigint REFERENCES ${a} (${a.columns.id}),
|
|
49
|
+
a_code text,
|
|
50
|
+
FOREIGN KEY (a_code) REFERENCES ${a} (${a.columns.code}),
|
|
51
|
+
other bigint REFERENCES ${a}
|
|
52
|
+
)`;
|
|
53
|
+
expect(getTypedSlots(b)).toEqual([
|
|
54
|
+
{ index: 2, kind: "references", column: "a_id" },
|
|
55
|
+
{ index: 4, kind: "references", column: "a_code" },
|
|
56
|
+
]);
|
|
57
|
+
expect(Object.getOwnPropertySymbols(b).some((k) => Object.getOwnPropertyDescriptor(b, k)!.enumerable)).toBe(false);
|
|
58
|
+
const out = sqlSerializer.serialize(entities({ s, a, b })) as { primary: string };
|
|
59
|
+
expect(out.primary).not.toContain("references\", \"column");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a ClickHouse sort key's bare interpolated columns, and nothing inside an expression", () => {
|
|
63
|
+
const db = ch.database`CREATE DATABASE shop`;
|
|
64
|
+
const e = ch.table`CREATE TABLE ${db}.e (k String, c Nullable(String), n UInt8) ENGINE = MergeTree ORDER BY k`;
|
|
65
|
+
const v = ch.view`
|
|
66
|
+
CREATE MATERIALIZED VIEW ${db}.v ENGINE = MergeTree
|
|
67
|
+
ORDER BY (${e.columns.k}, ifNull(${e.columns.c}, ''))
|
|
68
|
+
PRIMARY KEY ${e.columns.k}
|
|
69
|
+
AS SELECT ${e.columns.k}, ${e.columns.c} FROM ${e}`;
|
|
70
|
+
expect(getTypedSlots(v)).toEqual([
|
|
71
|
+
{ index: 1, kind: "sortKey" },
|
|
72
|
+
{ index: 3, kind: "sortKey" },
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("renderSqlTypes", () => {
|
|
78
|
+
test("keys are qualified and folded, a view passes its column through, slots stop at the last checked one", () => {
|
|
79
|
+
const s = pg.schema`CREATE SCHEMA Shop`;
|
|
80
|
+
const users = pg.table`CREATE TABLE ${s}.Users (Id serial PRIMARY KEY, mood text, born date NOT NULL)`;
|
|
81
|
+
const posts = pg.table`CREATE TABLE ${s}.posts (author integer REFERENCES ${users} (${users.columns.id}), body text)`;
|
|
82
|
+
const feed = pg.view`CREATE VIEW ${s}.feed AS SELECT p.${posts.columns.body}, upper(p.${posts.columns.body}) AS loud FROM ${posts} p`;
|
|
83
|
+
const text = renderSqlTypes(entities({ s, users, posts, feed }), { projectDir: "/" });
|
|
84
|
+
expect(text.split("\n")[0]).toContain("Generated by chant");
|
|
85
|
+
expect(text).toContain('import type { Column, KeyColumn } from "@intentius/chant-lexicon-sql/postgres";');
|
|
86
|
+
expect(text).toContain('"shop.users": {');
|
|
87
|
+
expect(text).toContain('id: Column<"integer", { ts: number; nullable: false; cmp: "N" }>;');
|
|
88
|
+
expect(text).toContain('born: Column<"date", { ts: Date; nullable: false; cmp: "D" }>;');
|
|
89
|
+
expect(text).toContain("slots: [unknown, unknown, KeyColumn<\"N\">];");
|
|
90
|
+
expect(text).toContain('body: Column<"text", { ts: string; nullable: true; cmp: "S" }>;');
|
|
91
|
+
expect(text).toContain('loud: Column<"unknown", { ts: unknown; nullable: never; cmp: never }>;');
|
|
92
|
+
expect(text.indexOf('"shop.feed"')).toBeLessThan(text.indexOf('"shop.posts"'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("with nothing declared, a module that augments nothing", () => {
|
|
96
|
+
expect(renderSqlTypes(new Map(), { config: { sql: { dialect: "postgres" } } })).toBe(
|
|
97
|
+
[
|
|
98
|
+
"// Generated by chant — do not edit directly. `chant sql types` writes it from the build's entities.",
|
|
99
|
+
'import type {} from "@intentius/chant-lexicon-sql/postgres";',
|
|
100
|
+
"",
|
|
101
|
+
'declare module "@intentius/chant-lexicon-sql/postgres" {',
|
|
102
|
+
" interface SqlSchema {}",
|
|
103
|
+
"}",
|
|
104
|
+
"",
|
|
105
|
+
].join("\n"),
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("relationKey: qualified when the DDL qualifies it", () => {
|
|
110
|
+
expect(relationKey({ schema: "app", name: "users" })).toBe("app.users");
|
|
111
|
+
expect(relationKey({ database: "shop", name: "events" })).toBe("shop.events");
|
|
112
|
+
expect(relationKey({ name: "events" })).toBe("events");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("chant sql types", () => {
|
|
117
|
+
test("refuses to write beside a sql-types.ts, which tsc would read instead", async () => {
|
|
118
|
+
const dir = mkdtempSync(join(tmpdir(), "sql-types-"));
|
|
119
|
+
writeFileSync(join(dir, "sql-types.ts"), "export {};\n");
|
|
120
|
+
expect(shadowingSource(join(dir, "sql-types.d.ts"))).toBe(join(dir, "sql-types.ts"));
|
|
121
|
+
expect(shadowingSource(join(dir, "other.d.ts"))).toBeUndefined();
|
|
122
|
+
const cwd = process.cwd();
|
|
123
|
+
process.chdir(dir);
|
|
124
|
+
try {
|
|
125
|
+
expect(await runTypes({ verb: "types", rawArgs: ["--output", "sql-types.d.ts"] })).toBe(1);
|
|
126
|
+
expect(await runTypes({ verb: "types", rawArgs: ["--bogus"] })).toBe(1);
|
|
127
|
+
} finally {
|
|
128
|
+
process.chdir(cwd);
|
|
129
|
+
}
|
|
130
|
+
expect(readFileSync(join(dir, "sql-types.ts"), "utf8")).toBe("export {};\n");
|
|
131
|
+
});
|
|
132
|
+
});
|