dynalitics 0.0.0-stage → 0.1.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.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -2
  3. package/dist/adapter-version.d.ts +3 -0
  4. package/dist/adapter-version.js +3 -0
  5. package/dist/adapter.d.ts +12 -0
  6. package/dist/adapter.js +7 -0
  7. package/dist/cli/io.d.ts +9 -0
  8. package/dist/cli/io.js +10 -0
  9. package/dist/cli/run.d.ts +4 -0
  10. package/dist/cli/run.js +39 -0
  11. package/dist/cli/sql.d.ts +5 -0
  12. package/dist/cli/sql.js +129 -0
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.js +5 -0
  15. package/dist/config/schema.d.ts +64 -0
  16. package/dist/config/schema.js +248 -0
  17. package/dist/config/validate.d.ts +30 -0
  18. package/dist/config/validate.js +68 -0
  19. package/dist/create.d.ts +16 -0
  20. package/dist/create.js +308 -0
  21. package/dist/dialect/common.d.ts +54 -0
  22. package/dist/dialect/common.js +197 -0
  23. package/dist/dialect/index.d.ts +5 -0
  24. package/dist/dialect/index.js +8 -0
  25. package/dist/dialect/mssql.d.ts +3 -0
  26. package/dist/dialect/mssql.js +104 -0
  27. package/dist/dialect/mysql.d.ts +3 -0
  28. package/dist/dialect/mysql.js +140 -0
  29. package/dist/dialect/postgres.d.ts +5 -0
  30. package/dist/dialect/postgres.js +90 -0
  31. package/dist/dialect/types.d.ts +79 -0
  32. package/dist/dialect/types.js +2 -0
  33. package/dist/driver/types.d.ts +74 -0
  34. package/dist/driver/types.js +66 -0
  35. package/dist/errors.d.ts +39 -0
  36. package/dist/errors.js +39 -0
  37. package/dist/index.d.ts +7 -0
  38. package/dist/index.js +3 -0
  39. package/dist/internals.d.ts +20 -0
  40. package/dist/internals.js +8 -0
  41. package/dist/logger.d.ts +10 -0
  42. package/dist/logger.js +24 -0
  43. package/dist/model/column-model.d.ts +7 -0
  44. package/dist/model/column-model.js +240 -0
  45. package/dist/model/identifiers.d.ts +19 -0
  46. package/dist/model/identifiers.js +62 -0
  47. package/dist/model/physical.d.ts +19 -0
  48. package/dist/model/physical.js +70 -0
  49. package/dist/model/types.d.ts +92 -0
  50. package/dist/model/types.js +9 -0
  51. package/dist/protocol.d.ts +222 -0
  52. package/dist/protocol.js +5 -0
  53. package/dist/queue/pipeline.d.ts +46 -0
  54. package/dist/queue/pipeline.js +282 -0
  55. package/dist/read/adapters.d.ts +7 -0
  56. package/dist/read/adapters.js +75 -0
  57. package/dist/read/buckets.d.ts +7 -0
  58. package/dist/read/buckets.js +39 -0
  59. package/dist/read/capabilities.d.ts +39 -0
  60. package/dist/read/capabilities.js +65 -0
  61. package/dist/read/handler.d.ts +48 -0
  62. package/dist/read/handler.js +63 -0
  63. package/dist/read/http.d.ts +18 -0
  64. package/dist/read/http.js +65 -0
  65. package/dist/read/index.d.ts +5 -0
  66. package/dist/read/index.js +3 -0
  67. package/dist/read/lock/authorize.d.ts +22 -0
  68. package/dist/read/lock/authorize.js +32 -0
  69. package/dist/read/lock/base-path.d.ts +11 -0
  70. package/dist/read/lock/base-path.js +12 -0
  71. package/dist/read/lock/gate.d.ts +30 -0
  72. package/dist/read/lock/gate.js +24 -0
  73. package/dist/read/lock/index.d.ts +28 -0
  74. package/dist/read/lock/index.js +46 -0
  75. package/dist/read/lock/options.d.ts +13 -0
  76. package/dist/read/lock/options.js +18 -0
  77. package/dist/read/lock/serve-html.d.ts +6 -0
  78. package/dist/read/lock/serve-html.js +38 -0
  79. package/dist/read/mask.d.ts +12 -0
  80. package/dist/read/mask.js +55 -0
  81. package/dist/read/params.d.ts +56 -0
  82. package/dist/read/params.js +150 -0
  83. package/dist/read/physical.d.ts +11 -0
  84. package/dist/read/physical.js +23 -0
  85. package/dist/read/queries.d.ts +75 -0
  86. package/dist/read/queries.js +316 -0
  87. package/dist/read/report.d.ts +21 -0
  88. package/dist/read/report.js +32 -0
  89. package/dist/read/routes.d.ts +38 -0
  90. package/dist/read/routes.js +261 -0
  91. package/dist/read/scope.d.ts +11 -0
  92. package/dist/read/scope.js +47 -0
  93. package/dist/read/search/fields.d.ts +24 -0
  94. package/dist/read/search/fields.js +67 -0
  95. package/dist/read/search/parse.d.ts +48 -0
  96. package/dist/read/search/parse.js +124 -0
  97. package/dist/read/semaphore.d.ts +6 -0
  98. package/dist/read/semaphore.js +34 -0
  99. package/dist/read/service.d.ts +42 -0
  100. package/dist/read/service.js +266 -0
  101. package/dist/read/types.d.ts +62 -0
  102. package/dist/read/types.js +2 -0
  103. package/dist/redact.d.ts +8 -0
  104. package/dist/redact.js +60 -0
  105. package/dist/report.d.ts +12 -0
  106. package/dist/report.js +39 -0
  107. package/dist/schema/generate.d.ts +46 -0
  108. package/dist/schema/generate.js +71 -0
  109. package/dist/schema/index.d.ts +5 -0
  110. package/dist/schema/index.js +3 -0
  111. package/dist/startup/compare.d.ts +22 -0
  112. package/dist/startup/compare.js +165 -0
  113. package/dist/startup/fix-sql.d.ts +15 -0
  114. package/dist/startup/fix-sql.js +94 -0
  115. package/dist/startup/privileges.d.ts +21 -0
  116. package/dist/startup/privileges.js +63 -0
  117. package/dist/startup/session-timezone.d.ts +10 -0
  118. package/dist/startup/session-timezone.js +19 -0
  119. package/dist/startup/type-families.d.ts +10 -0
  120. package/dist/startup/type-families.js +55 -0
  121. package/dist/startup/unmapped-notice.d.ts +5 -0
  122. package/dist/startup/unmapped-notice.js +28 -0
  123. package/dist/track/build-row.d.ts +50 -0
  124. package/dist/track/build-row.js +208 -0
  125. package/dist/track/custom.d.ts +31 -0
  126. package/dist/track/custom.js +83 -0
  127. package/dist/track/denied-view.d.ts +14 -0
  128. package/dist/track/denied-view.js +21 -0
  129. package/dist/track/first-visit.d.ts +19 -0
  130. package/dist/track/first-visit.js +70 -0
  131. package/dist/track/ip.d.ts +17 -0
  132. package/dist/track/ip.js +87 -0
  133. package/dist/track/limits.d.ts +7 -0
  134. package/dist/track/limits.js +26 -0
  135. package/dist/types.d.ts +136 -0
  136. package/dist/types.js +2 -0
  137. package/dist/version.d.ts +3 -0
  138. package/dist/version.js +3 -0
  139. package/package.json +71 -4
@@ -0,0 +1,54 @@
1
+ import { type TableRef } from '../model/identifiers.js';
2
+ import type { ColumnDef, ColumnModel, Dialect } from '../model/types.js';
3
+ import type { CompiledQuery, ReadSyntax, Row, SqlDialect } from './types.js';
4
+ export declare const PLACEHOLDER_ROLE: RegExp;
5
+ declare const UNLIMITED: number;
6
+ /** Everything that differs between dialects. The generic builder below turns this into a SqlDialect. */
7
+ export interface DialectSyntax {
8
+ readonly name: Dialect;
9
+ readonly limits: SqlDialect['limits'];
10
+ readonly open: string;
11
+ readonly close: string;
12
+ readonly explicitNull: boolean;
13
+ readonly read: ReadSyntax;
14
+ placeholder(index: number): string;
15
+ jsonCast?(placeholder: string): string;
16
+ timestampParam(d: Date): string;
17
+ columnType(c: ColumnDef): string;
18
+ /** Full definition for the id column, constraints included. */
19
+ idDefinition: string;
20
+ /** Collation clause that goes right after the type ('' if none). */
21
+ collation(c: ColumnDef): string;
22
+ /** Constraint that goes after nullability, e.g. SQL Server's ISJSON check ('' if none). */
23
+ constraint(c: ColumnDef, model: ColumnModel): string;
24
+ tableSuffix(): string;
25
+ tableConstraints(model: ColumnModel, quote: (n: string) => string): readonly string[];
26
+ addColumnKeyword: string;
27
+ dropNotNull(table: string, name: string, type: string): string;
28
+ alterType(table: string, name: string, type: string): string;
29
+ supportsPartialIndex: boolean;
30
+ introspectSql(table: TableRef, q: (n: string) => string): CompiledQuery;
31
+ privilegeSql(table: TableRef, qualified: string): readonly CompiledQuery[];
32
+ parsePrivileges(results: readonly (readonly Row[])[], table: TableRef): ReadonlySet<string>;
33
+ grantTarget(table: TableRef, q: (n: string) => string): string;
34
+ grantSelectColumns(target: string, columns: readonly string[], role: string, q: (n: string) => string): string;
35
+ renderRole(role: string, q: (n: string) => string): string;
36
+ grantPreamble(table: TableRef, writer: string, reader: string, q: (n: string) => string): readonly string[];
37
+ }
38
+ export declare function indexName(table: string, suffix: string, max: number): string;
39
+ interface IndexSpec {
40
+ readonly suffix: string;
41
+ readonly columns: readonly string[];
42
+ readonly whereNotNull?: string;
43
+ }
44
+ /** Recommended indexes (spec 6.3): range scans, and the "any earlier row?" probes of read-time first-visit. */
45
+ export declare function indexSpecs(model: ColumnModel): readonly IndexSpec[];
46
+ export declare const qualify: (t: TableRef, q: (n: string) => string) => string;
47
+ export declare function createDialect(s: DialectSyntax): SqlDialect;
48
+ /** Shared role rendering: placeholders like <writer_role> pass through, real names must be valid identifiers. */
49
+ export declare function renderRoleWith(role: string, wrap: (name: string) => string): string;
50
+ export { UNLIMITED };
51
+ /** Guards for the few non-identifier values that are inlined into read SQL. */
52
+ export declare function assertLimit(n: number): number;
53
+ export declare function assertJsonKey(key: string): string;
54
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1,197 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { DynaliticsConfigError } from '../errors.js';
3
+ import { assertIdentifier, isCustomKey, isIdentifier, } from '../model/identifiers.js';
4
+ import { isIntegerColumn } from '../model/physical.js';
5
+ export const PLACEHOLDER_ROLE = /^<[a-z_]+>$/;
6
+ const UNLIMITED = Number.MAX_SAFE_INTEGER;
7
+ const toBool = (v) => v === true || v === 1 || v === '1' || v === 't' || v === 'YES';
8
+ const toNum = (v) => v === null || v === undefined || v === '' ? null : Number.isFinite(Number(v)) ? Number(v) : null;
9
+ function describeComment(c) {
10
+ if (c.role === 'isFirstVisit')
11
+ return '-- derived from personal identifiers; NULL without consent';
12
+ if (c.role === 'custom')
13
+ return '-- declared custom fields; personal ones are NULL without consent';
14
+ if (c.role === 'ip')
15
+ return '-- PERSONAL DATA (stored per ip.mode)';
16
+ return c.personal ? '-- PERSONAL DATA' : '';
17
+ }
18
+ export function indexName(table, suffix, max) {
19
+ const full = `${table}_${suffix}`;
20
+ if (full.length <= max)
21
+ return full;
22
+ const digest = createHash('sha1').update(full).digest('hex').slice(0, 8);
23
+ return `${full.slice(0, max - 9)}_${digest}`;
24
+ }
25
+ /** Recommended indexes (spec 6.3): range scans, and the "any earlier row?" probes of read-time first-visit. */
26
+ export function indexSpecs(model) {
27
+ const ts = model.columns.find((c) => c.role === 'timestamp');
28
+ const id = model.columns.find((c) => c.role === 'id');
29
+ if (ts === undefined)
30
+ return [];
31
+ const specs = [
32
+ { suffix: 'visited_at_idx', columns: id === undefined ? [ts.name] : [ts.name, id.name] },
33
+ ];
34
+ const { userId, ip } = model.identity;
35
+ if (userId !== undefined) {
36
+ specs.push({
37
+ suffix: 'user_id_idx',
38
+ columns: [userId.name, ts.name],
39
+ whereNotNull: userId.name,
40
+ });
41
+ }
42
+ if (ip !== undefined)
43
+ specs.push({ suffix: 'ip_idx', columns: [ip.name, ts.name], whereNotNull: ip.name });
44
+ return specs;
45
+ }
46
+ export const qualify = (t, q) => [t.database, t.schema, t.name]
47
+ .filter((p) => p !== undefined)
48
+ .map(q)
49
+ .join('.');
50
+ export function createDialect(s) {
51
+ const quoteIdent = (name) => {
52
+ assertIdentifier(name, 'identifier');
53
+ return `${s.open}${name}${s.close}`;
54
+ };
55
+ const qualifiedTable = (t) => [t.database, t.schema, t.name]
56
+ .filter((p) => p !== undefined)
57
+ .map(quoteIdent)
58
+ .join('.');
59
+ const columnType = (c) => s.columnType(c);
60
+ const columnDefinition = (c, model) => {
61
+ if (c.type.kind === 'id')
62
+ return s.idDefinition;
63
+ const nullPart = c.nullable ? (s.explicitNull ? 'NULL' : '') : 'NOT NULL';
64
+ return [`${columnType(c)} ${s.collation(c)}`.trim(), nullPart, s.constraint(c, model)]
65
+ .filter((part) => part !== '')
66
+ .join(' ');
67
+ };
68
+ return {
69
+ name: s.name,
70
+ limits: s.limits,
71
+ quoteIdent,
72
+ qualifiedTable,
73
+ read: s.read,
74
+ placeholder: (i) => s.placeholder(i),
75
+ columnType,
76
+ timestampParam: (d) => s.timestampParam(d),
77
+ buildInsert(model, rows) {
78
+ const cols = model.insertColumns;
79
+ const params = [];
80
+ const tuples = rows.map((row) => {
81
+ if (row.length !== cols.length) {
82
+ throw new Error(`expected ${cols.length} values per row, got ${row.length}`);
83
+ }
84
+ const phs = row.map((value, idx) => {
85
+ // A boolean headed for a real integer column (tinyint(1), smallint) is bound as 1/0.
86
+ const target = cols[idx];
87
+ params.push(typeof value === 'boolean' && target?.type.kind === 'boolean' && isIntegerColumn(target)
88
+ ? value
89
+ ? 1
90
+ : 0
91
+ : value);
92
+ const ph = s.placeholder(params.length);
93
+ return cols[idx]?.type.kind === 'json' && s.jsonCast !== undefined ? s.jsonCast(ph) : ph;
94
+ });
95
+ return `(${phs.join(', ')})`;
96
+ });
97
+ return {
98
+ sql: `INSERT INTO ${qualifiedTable(model.table)} (${cols.map((c) => quoteIdent(c.name)).join(', ')}) VALUES ${tuples.join(', ')}`,
99
+ params,
100
+ };
101
+ },
102
+ firstVisitLookup(model, kind, values) {
103
+ const col = kind === 'u' ? model.identity.userId : model.identity.ip;
104
+ if (col === undefined)
105
+ throw new Error('identity column is not mapped');
106
+ const phs = values.map((_, i) => s.placeholder(i + 1)).join(', ');
107
+ const userCol = model.identity.userId;
108
+ const extra = kind === 'i' && userCol !== undefined ? ` AND ${quoteIdent(userCol.name)} IS NULL` : '';
109
+ return {
110
+ sql: `SELECT DISTINCT ${quoteIdent(col.name)} AS v FROM ${qualifiedTable(model.table)} WHERE ${quoteIdent(col.name)} IN (${phs})${extra}`,
111
+ params: [...values],
112
+ };
113
+ },
114
+ introspectSql: (t) => s.introspectSql(t, quoteIdent),
115
+ mapColumnRow(row) {
116
+ const maxLength = toNum(row['max_length']);
117
+ return {
118
+ name: String(row['name']),
119
+ dataType: String(row['data_type']).toLowerCase(),
120
+ maxLength,
121
+ nullable: toBool(row['is_nullable']),
122
+ hasDefault: toBool(row['has_default']),
123
+ generated: toBool(row['is_generated']),
124
+ precision: toNum(row['dt_precision']),
125
+ tableType: typeof row['table_type'] === 'string' ? row['table_type'] : 'BASE TABLE',
126
+ ...(typeof row['schema_name'] === 'string' ? { schema: row['schema_name'] } : {}),
127
+ };
128
+ },
129
+ privilegeSql: (t) => s.privilegeSql(t, qualifiedTable(t)),
130
+ parsePrivileges: (results, t) => s.parsePrivileges(results, t),
131
+ createTableSql(model) {
132
+ const lines = model.columns.map((c) => ({
133
+ text: `${quoteIdent(c.name).padEnd(18)} ${columnDefinition(c, model)}`,
134
+ comment: describeComment(c),
135
+ }));
136
+ const constraints = s.tableConstraints(model, quoteIdent);
137
+ const all = [...lines, ...constraints.map((text) => ({ text, comment: '' }))];
138
+ const body = all
139
+ .map((l, i) => ` ${l.text}${i < all.length - 1 ? ',' : ''}${l.comment === '' ? '' : ` ${l.comment}`}`)
140
+ .join('\n');
141
+ return `CREATE TABLE ${qualifiedTable(model.table)} (\n${body}\n)${s.tableSuffix()};`;
142
+ },
143
+ createIndexSql(model) {
144
+ const table = qualifiedTable(model.table);
145
+ return indexSpecs(model).map((spec) => {
146
+ const name = quoteIdent(indexName(model.table.name, spec.suffix, s.limits.maxIdentifier));
147
+ const where = spec.whereNotNull !== undefined && s.supportsPartialIndex
148
+ ? ` WHERE ${quoteIdent(spec.whereNotNull)} IS NOT NULL`
149
+ : '';
150
+ return `CREATE INDEX ${name} ON ${table} (${spec.columns.map(quoteIdent).join(', ')})${where};`;
151
+ });
152
+ },
153
+ grantSql(opts) {
154
+ const writer = s.renderRole(opts.writerRole, quoteIdent);
155
+ const reader = s.renderRole(opts.readerRole, quoteIdent);
156
+ const target = s.grantTarget(opts.table, quoteIdent);
157
+ const out = [...s.grantPreamble(opts.table, writer, reader, quoteIdent)];
158
+ out.push('-- Application (writer): INSERT only. It cannot read anything back.');
159
+ out.push(`GRANT INSERT ON ${target} TO ${writer};`);
160
+ if (opts.writerSelectColumns !== undefined && opts.writerSelectColumns.length > 0) {
161
+ out.push('-- Needed only because firstVisit.enabled is on (write-time detection): column-level SELECT on identity columns.');
162
+ out.push(s.grantSelectColumns(target, opts.writerSelectColumns, writer, quoteIdent));
163
+ }
164
+ out.push('-- Dashboard (reader): SELECT only. This role can see PERSONAL DATA columns; protect it accordingly.');
165
+ out.push(`GRANT SELECT ON ${target} TO ${reader};`);
166
+ return out;
167
+ },
168
+ addColumnSql: (t, c) => `ALTER TABLE ${qualifiedTable(t)} ${s.addColumnKeyword} ${quoteIdent(c.name)} ${columnType(c)};`,
169
+ dropNotNullSql: (t, c) => s.dropNotNull(qualifiedTable(t), quoteIdent(c.name), columnType(c)),
170
+ alterTypeSql: (t, c) => s.alterType(qualifiedTable(t), quoteIdent(c.name), columnType(c)),
171
+ renderRole: (role) => s.renderRole(role, quoteIdent),
172
+ };
173
+ }
174
+ /** Shared role rendering: placeholders like <writer_role> pass through, real names must be valid identifiers. */
175
+ export function renderRoleWith(role, wrap) {
176
+ if (PLACEHOLDER_ROLE.test(role))
177
+ return role;
178
+ if (!isIdentifier(role)) {
179
+ throw new DynaliticsConfigError([
180
+ `Role ${JSON.stringify(role)} is not supported. Use letters, digits and underscore (max 63), or leave the <placeholder>.`,
181
+ ]);
182
+ }
183
+ return wrap(role);
184
+ }
185
+ export { UNLIMITED };
186
+ /** Guards for the few non-identifier values that are inlined into read SQL. */
187
+ export function assertLimit(n) {
188
+ if (!Number.isInteger(n) || n < 1 || n > 100_000)
189
+ throw new Error('limit must be a positive integer');
190
+ return n;
191
+ }
192
+ export function assertJsonKey(key) {
193
+ if (!isCustomKey(key))
194
+ throw new Error('invalid custom field key');
195
+ return key;
196
+ }
197
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1,5 @@
1
+ import type { Dialect } from '../model/types.js';
2
+ import type { SqlDialect } from './types.js';
3
+ export declare function getDialect(name: Dialect): SqlDialect;
4
+ export type { SqlDialect, CompiledQuery, ColumnInfo, Row, GrantOptions } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,8 @@
1
+ import { mssql } from './mssql.js';
2
+ import { mysql } from './mysql.js';
3
+ import { postgres } from './postgres.js';
4
+ const DIALECTS = { postgres, mysql, mssql };
5
+ export function getDialect(name) {
6
+ return DIALECTS[name];
7
+ }
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { SqlDialect } from './types.js';
2
+ export declare const mssql: SqlDialect;
3
+ //# sourceMappingURL=mssql.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { assertJsonKey, assertLimit, createDialect, qualify, renderRoleWith } from './common.js';
2
+ const BIN_COLLATION = 'COLLATE Latin1_General_100_BIN2';
3
+ function columnType(c) {
4
+ switch (c.type.kind) {
5
+ case 'id':
6
+ return 'BIGINT';
7
+ case 'timestamp':
8
+ return 'DATETIME2(3)';
9
+ case 'boolean':
10
+ return 'BIT';
11
+ case 'string':
12
+ return `NVARCHAR(${c.type.maxLength})`;
13
+ case 'integer':
14
+ return 'BIGINT';
15
+ case 'number':
16
+ return 'FLOAT(53)';
17
+ case 'json':
18
+ return 'NVARCHAR(MAX)';
19
+ }
20
+ }
21
+ export const mssql = createDialect({
22
+ name: 'mssql',
23
+ limits: { maxParams: 2100, maxRowsPerInsert: 1000, maxIdentifier: 128 },
24
+ open: '[',
25
+ close: ']',
26
+ explicitNull: true,
27
+ read: {
28
+ bucket: (col, interval) => {
29
+ const floor = {
30
+ hour: `DATEADD(hour, DATEDIFF(hour, 0, ${col}), 0)`,
31
+ day: `DATEADD(day, DATEDIFF(day, 0, ${col}), 0)`,
32
+ // 1900-01-01 is a Monday and DATEDIFF(day) ignores DATEFIRST, so weeks never depend on session settings.
33
+ week: `DATEADD(day, (DATEDIFF(day, '19000101', ${col}) / 7) * 7, '19000101')`,
34
+ month: `DATEADD(month, DATEDIFF(month, 0, ${col}), 0)`,
35
+ }[interval];
36
+ // Style 126 drops the fraction when it is zero (always, for a bucket start), so the fraction is written
37
+ // explicitly: every bucket key must equal the JavaScript toISOString() form exactly (found by the first CI run).
38
+ return `CONVERT(varchar(19), ${floor}, 126) + '.000Z'`;
39
+ },
40
+ // Same reason: style 126 would print 12:01:44Z for a visit at exactly .000 seconds.
41
+ tsText: (col) => `CONVERT(varchar(19), ${col}, 126) + '.' + RIGHT('00' + CAST(DATEPART(millisecond, ${col}) AS varchar(3)), 3) + 'Z'`,
42
+ isTrue: (col) => `${col} = 1`,
43
+ isFalse: (col) => `${col} = 0`,
44
+ jsonKey: (col, key) => `JSON_VALUE(${col}, '$.${assertJsonKey(key)}')`,
45
+ jsonNumber: (col, key) => `TRY_CAST(JSON_VALUE(${col}, '$.${assertJsonKey(key)}') AS float)`,
46
+ castText: (col, kind) => kind === 'string'
47
+ ? col
48
+ : kind === 'boolean'
49
+ ? `CASE WHEN ${col} = 1 THEN 'true' WHEN ${col} = 0 THEN 'false' END`
50
+ : `CAST(${col} AS NVARCHAR(4000))`,
51
+ count: 'COUNT_BIG(*)',
52
+ top: (n) => `TOP ${assertLimit(n)} `,
53
+ limit: () => '',
54
+ },
55
+ placeholder: (i) => `@p${i}`,
56
+ timestampParam: (d) => d.toISOString().replace('Z', ''),
57
+ columnType,
58
+ idDefinition: 'BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY',
59
+ collation: (c) => c.role === 'pageVisited' || c.role === 'ip' || c.role === 'userId' ? BIN_COLLATION : '',
60
+ constraint: (c, model) => c.role === 'custom'
61
+ ? `CONSTRAINT [CK_${model.table.name}_custom_json] CHECK ([${c.name}] IS NULL OR ISJSON([${c.name}]) = 1)`
62
+ : '',
63
+ tableSuffix: () => '',
64
+ tableConstraints: () => [],
65
+ addColumnKeyword: 'ADD',
66
+ dropNotNull: (table, name, type) => `ALTER TABLE ${table} ALTER COLUMN ${name} ${type} NULL;`,
67
+ alterType: (table, name, type) => `ALTER TABLE ${table} ALTER COLUMN ${name} ${type} NULL;`,
68
+ supportsPartialIndex: true,
69
+ introspectSql: (table, q) => {
70
+ const catalog = table.database === undefined ? '' : `${q(table.database)}.`;
71
+ return {
72
+ sql: `SELECT c.COLUMN_NAME AS name, c.DATA_TYPE AS data_type, c.CHARACTER_MAXIMUM_LENGTH AS max_length,
73
+ CASE WHEN c.IS_NULLABLE = 'YES' THEN 1 ELSE 0 END AS is_nullable,
74
+ CASE WHEN c.COLUMN_DEFAULT IS NOT NULL THEN 1 ELSE 0 END AS has_default,
75
+ CASE WHEN c.DATA_TYPE = 'timestamp'
76
+ OR COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.TABLE_CATALOG) + '.' + QUOTENAME(c.TABLE_SCHEMA) + '.' + QUOTENAME(c.TABLE_NAME)), c.COLUMN_NAME, 'IsIdentity') = 1
77
+ OR COLUMNPROPERTY(OBJECT_ID(QUOTENAME(c.TABLE_CATALOG) + '.' + QUOTENAME(c.TABLE_SCHEMA) + '.' + QUOTENAME(c.TABLE_NAME)), c.COLUMN_NAME, 'IsComputed') = 1
78
+ THEN 1 ELSE 0 END AS is_generated,
79
+ c.DATETIME_PRECISION AS dt_precision, t.TABLE_TYPE AS table_type, c.TABLE_SCHEMA AS schema_name
80
+ FROM ${catalog}INFORMATION_SCHEMA.COLUMNS c
81
+ JOIN ${catalog}INFORMATION_SCHEMA.TABLES t
82
+ ON t.TABLE_CATALOG = c.TABLE_CATALOG AND t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
83
+ WHERE c.TABLE_SCHEMA = COALESCE(@p1, SCHEMA_NAME()) AND c.TABLE_NAME = @p2
84
+ ORDER BY c.ORDINAL_POSITION`,
85
+ params: [table.schema ?? null, table.name],
86
+ };
87
+ },
88
+ privilegeSql: (_table, qualified) => [
89
+ {
90
+ sql: `SELECT permission_name AS privilege FROM fn_my_permissions(@p1, 'OBJECT')`,
91
+ params: [qualified],
92
+ },
93
+ {
94
+ sql: `SELECT 'SUPERUSER' AS privilege WHERE IS_SRVROLEMEMBER('sysadmin') = 1 OR IS_MEMBER('db_owner') = 1`,
95
+ params: [],
96
+ },
97
+ ],
98
+ parsePrivileges: (results) => new Set(results.flat().map((r) => String(r['privilege']).toUpperCase())),
99
+ grantTarget: (table, q) => `OBJECT::${qualify(table, q)}`,
100
+ grantSelectColumns: (target, columns, role, q) => `GRANT SELECT ON ${target} (${columns.map(q).join(', ')}) TO ${role};`,
101
+ renderRole: (role, q) => renderRoleWith(role, q),
102
+ grantPreamble: () => [],
103
+ });
104
+ //# sourceMappingURL=mssql.js.map
@@ -0,0 +1,3 @@
1
+ import type { SqlDialect } from './types.js';
2
+ export declare const mysql: SqlDialect;
3
+ //# sourceMappingURL=mysql.d.ts.map
@@ -0,0 +1,140 @@
1
+ import { UNLIMITED, assertJsonKey, assertLimit, createDialect, qualify, renderRoleWith, } from './common.js';
2
+ function columnType(c) {
3
+ switch (c.type.kind) {
4
+ case 'id':
5
+ return 'BIGINT UNSIGNED';
6
+ case 'timestamp':
7
+ return 'DATETIME(3)';
8
+ case 'boolean':
9
+ return 'TINYINT(1)';
10
+ case 'string':
11
+ return `VARCHAR(${c.type.maxLength})`;
12
+ case 'integer':
13
+ return 'BIGINT';
14
+ case 'number':
15
+ return 'DOUBLE';
16
+ case 'json':
17
+ return 'JSON';
18
+ }
19
+ }
20
+ const ALL_PRIVILEGES = [
21
+ 'SELECT',
22
+ 'INSERT',
23
+ 'UPDATE',
24
+ 'DELETE',
25
+ 'DROP',
26
+ 'ALTER',
27
+ 'CREATE',
28
+ 'REFERENCES',
29
+ 'TRIGGER',
30
+ ];
31
+ /** Best effort: parses `SHOW GRANTS` text for grants that apply to this table. Roles make it heuristic. */
32
+ function parseGrants(results, table) {
33
+ const text = (v) => (typeof v === 'string' ? v : '');
34
+ const grants = (results[0] ?? []).map((r) => text(Object.values(r)[0]));
35
+ const currentDb = text(results[1]?.[0]?.['db']);
36
+ const db = table.schema ?? currentDb;
37
+ const unescape = (s) => s.replace(/\\([_%])/g, '$1');
38
+ const found = new Set();
39
+ for (const line of grants) {
40
+ const m = /^GRANT (.+?) ON (.+?) TO /.exec(line);
41
+ if (m === null)
42
+ continue;
43
+ const scope = (m[2] ?? '').replace(/`/g, '');
44
+ const [scopeDb = '', scopeTable = ''] = scope.split('.');
45
+ const applies = scope === '*.*' ||
46
+ (unescape(scopeDb) === db && (scopeTable === '*' || scopeTable === table.name));
47
+ if (!applies)
48
+ continue;
49
+ const privs = (m[1] ?? '')
50
+ .replace(/\([^)]*\)/g, '')
51
+ .split(',')
52
+ .map((p) => p.trim().toUpperCase());
53
+ for (const p of privs) {
54
+ if (p === 'ALL PRIVILEGES' || p === 'ALL')
55
+ ALL_PRIVILEGES.forEach((x) => found.add(x));
56
+ else if (p !== 'USAGE' && p !== '')
57
+ found.add(p);
58
+ }
59
+ if (/WITH GRANT OPTION\s*$/.test(line))
60
+ found.add('GRANT OPTION');
61
+ }
62
+ return found;
63
+ }
64
+ export const mysql = createDialect({
65
+ name: 'mysql',
66
+ limits: { maxParams: 65_535, maxRowsPerInsert: UNLIMITED, maxIdentifier: 64 },
67
+ open: '`',
68
+ close: '`',
69
+ explicitNull: false,
70
+ read: {
71
+ bucket: (col, interval) => {
72
+ switch (interval) {
73
+ case 'hour':
74
+ return `DATE_FORMAT(${col}, '%Y-%m-%dT%H:00:00.000Z')`;
75
+ case 'day':
76
+ return `DATE_FORMAT(${col}, '%Y-%m-%dT00:00:00.000Z')`;
77
+ case 'week':
78
+ return `DATE_FORMAT(DATE_SUB(DATE(${col}), INTERVAL WEEKDAY(${col}) DAY), '%Y-%m-%dT00:00:00.000Z')`;
79
+ case 'month':
80
+ return `DATE_FORMAT(${col}, '%Y-%m-01T00:00:00.000Z')`;
81
+ }
82
+ },
83
+ tsText: (col) => `CONCAT(DATE_FORMAT(${col}, '%Y-%m-%dT%H:%i:%s.'), LPAD(MICROSECOND(${col}) DIV 1000, 3, '0'), 'Z')`,
84
+ isTrue: (col) => `${col} = 1`,
85
+ isFalse: (col) => `${col} = 0`,
86
+ jsonKey: (col, key) => `JSON_UNQUOTE(JSON_EXTRACT(${col}, '$.${assertJsonKey(key)}'))`,
87
+ jsonNumber: (col, key) => {
88
+ const v = `JSON_EXTRACT(${col}, '$.${assertJsonKey(key)}')`;
89
+ return `CASE WHEN JSON_TYPE(${v}) IN ('INTEGER', 'UNSIGNED INTEGER', 'DOUBLE', 'DECIMAL') THEN CAST(${v} AS DOUBLE) END`;
90
+ },
91
+ castText: (col, kind) => kind === 'string'
92
+ ? col
93
+ : kind === 'boolean'
94
+ ? `CASE WHEN ${col} = 1 THEN 'true' WHEN ${col} = 0 THEN 'false' END`
95
+ : `CAST(${col} AS CHAR)`,
96
+ count: 'COUNT(*)',
97
+ top: () => '',
98
+ limit: (n) => ` LIMIT ${assertLimit(n)}`,
99
+ },
100
+ placeholder: () => '?',
101
+ timestampParam: (d) => d.toISOString().replace('T', ' ').replace('Z', ''),
102
+ columnType,
103
+ idDefinition: 'BIGINT UNSIGNED NOT NULL AUTO_INCREMENT',
104
+ collation: () => '',
105
+ constraint: () => '',
106
+ tableSuffix: () => ' ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin',
107
+ tableConstraints: (model, q) => {
108
+ const id = model.columns.find((c) => c.role === 'id');
109
+ return id === undefined ? [] : [`PRIMARY KEY (${q(id.name)})`];
110
+ },
111
+ addColumnKeyword: 'ADD COLUMN',
112
+ dropNotNull: (table, name, type) => `ALTER TABLE ${table} MODIFY COLUMN ${name} ${type} NULL;`,
113
+ alterType: (table, name, type) => `ALTER TABLE ${table} MODIFY COLUMN ${name} ${type};`,
114
+ supportsPartialIndex: false,
115
+ introspectSql: (table) => ({
116
+ sql: `SELECT c.COLUMN_NAME AS name, c.DATA_TYPE AS data_type, c.CHARACTER_MAXIMUM_LENGTH AS max_length,
117
+ (c.IS_NULLABLE = 'YES') AS is_nullable, (c.COLUMN_DEFAULT IS NOT NULL) AS has_default,
118
+ (c.EXTRA LIKE '%auto_increment%' OR c.EXTRA LIKE '%GENERATED%') AS is_generated,
119
+ c.DATETIME_PRECISION AS dt_precision, t.TABLE_TYPE AS table_type, c.TABLE_SCHEMA AS schema_name
120
+ FROM information_schema.COLUMNS c
121
+ JOIN information_schema.TABLES t ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
122
+ WHERE c.TABLE_SCHEMA = COALESCE(?, DATABASE()) AND c.TABLE_NAME = ?
123
+ ORDER BY c.ORDINAL_POSITION`,
124
+ params: [table.schema ?? null, table.name],
125
+ }),
126
+ privilegeSql: () => [
127
+ { sql: 'SHOW GRANTS FOR CURRENT_USER()', params: [] },
128
+ { sql: 'SELECT DATABASE() AS db', params: [] },
129
+ ],
130
+ parsePrivileges: (results, table) => parseGrants(results, table),
131
+ grantTarget: (table, q) => (table.schema === undefined ? q(table.name) : qualify(table, q)),
132
+ grantSelectColumns: (target, columns, role, q) => `GRANT SELECT (${columns.map(q).join(', ')}) ON ${target} TO ${role};`,
133
+ renderRole: (role, q) => renderRoleWith(role, (name) => `${q(name)}@\`%\``),
134
+ grantPreamble: (table) => table.schema === undefined
135
+ ? [
136
+ '-- No schema given: these grants apply to the current database. Use schema.name to be explicit.',
137
+ ]
138
+ : [],
139
+ });
140
+ //# sourceMappingURL=mysql.js.map
@@ -0,0 +1,5 @@
1
+ import { PLACEHOLDER_ROLE } from './common.js';
2
+ import type { SqlDialect } from './types.js';
3
+ export declare const postgres: SqlDialect;
4
+ export { PLACEHOLDER_ROLE };
5
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1,90 @@
1
+ import { PLACEHOLDER_ROLE, UNLIMITED, assertJsonKey, assertLimit, createDialect, qualify, renderRoleWith, } from './common.js';
2
+ function columnType(c) {
3
+ switch (c.type.kind) {
4
+ case 'id':
5
+ return 'bigint';
6
+ case 'timestamp':
7
+ return 'timestamptz';
8
+ case 'boolean':
9
+ return 'boolean';
10
+ case 'string':
11
+ return `varchar(${c.type.maxLength})`;
12
+ case 'integer':
13
+ return 'bigint';
14
+ case 'number':
15
+ return 'double precision';
16
+ case 'json':
17
+ return 'jsonb';
18
+ }
19
+ }
20
+ export const postgres = createDialect({
21
+ name: 'postgres',
22
+ limits: { maxParams: 65_535, maxRowsPerInsert: UNLIMITED, maxIdentifier: 63 },
23
+ open: '"',
24
+ close: '"',
25
+ explicitNull: false,
26
+ read: {
27
+ bucket: (col, interval) => `to_char(date_trunc('${interval}', ${col} AT TIME ZONE 'UTC'), 'YYYY-MM-DD"T"HH24:MI:SS".000Z"')`,
28
+ tsText: (col) => `to_char(${col} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`,
29
+ isTrue: (col) => `${col} IS TRUE`,
30
+ isFalse: (col) => `${col} IS FALSE`,
31
+ jsonKey: (col, key) => `(${col} ->> '${assertJsonKey(key)}')`,
32
+ // SIMILAR TO, not ~: F-39 allows no '~' outside literals. PostgreSQL has no TRY_CAST.
33
+ jsonNumber: (col, key) => {
34
+ const v = `(${col} ->> '${assertJsonKey(key)}')`;
35
+ return `CASE WHEN ${v} SIMILAR TO '-?[0-9]+([.][0-9]+)?([eE][-+]?[0-9]+)?' THEN CAST(${v} AS double precision) END`;
36
+ },
37
+ castText: (col, kind) => kind === 'string'
38
+ ? col
39
+ : kind === 'boolean'
40
+ ? `CASE WHEN ${col} IS TRUE THEN 'true' WHEN ${col} IS FALSE THEN 'false' END`
41
+ : `CAST(${col} AS text)`,
42
+ count: 'COUNT(*)',
43
+ top: () => '',
44
+ limit: (n) => ` LIMIT ${assertLimit(n)}`,
45
+ },
46
+ placeholder: (i) => `$${i}`,
47
+ jsonCast: (ph) => `${ph}::jsonb`,
48
+ timestampParam: (d) => d.toISOString(),
49
+ columnType,
50
+ idDefinition: 'bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY',
51
+ collation: () => '',
52
+ constraint: () => '',
53
+ tableSuffix: () => '',
54
+ tableConstraints: () => [],
55
+ addColumnKeyword: 'ADD COLUMN',
56
+ dropNotNull: (table, name) => `ALTER TABLE ${table} ALTER COLUMN ${name} DROP NOT NULL;`,
57
+ alterType: (table, name, type) => `ALTER TABLE ${table} ALTER COLUMN ${name} TYPE ${type} USING ${name}::${type};`,
58
+ supportsPartialIndex: true,
59
+ introspectSql: (table) => ({
60
+ sql: `SELECT c.column_name AS name, c.udt_name AS data_type, c.character_maximum_length AS max_length,
61
+ (c.is_nullable = 'YES') AS is_nullable, (c.column_default IS NOT NULL) AS has_default,
62
+ (c.is_identity = 'YES' OR c.is_generated = 'ALWAYS') AS is_generated,
63
+ c.datetime_precision AS dt_precision, t.table_type AS table_type, c.table_schema AS schema_name
64
+ FROM information_schema.columns c
65
+ JOIN information_schema.tables t ON t.table_schema = c.table_schema AND t.table_name = c.table_name
66
+ WHERE c.table_schema = COALESCE($1::text, current_schema()) AND c.table_name = $2::text
67
+ ORDER BY c.ordinal_position`,
68
+ params: [table.schema ?? null, table.name],
69
+ }),
70
+ privilegeSql: (_table, qualified) => [
71
+ {
72
+ sql: `SELECT p AS privilege FROM unnest(ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER']) AS p
73
+ WHERE has_table_privilege(current_user, $1::text, p)`,
74
+ params: [qualified],
75
+ },
76
+ {
77
+ sql: `SELECT 'SUPERUSER' AS privilege FROM pg_roles WHERE rolname = current_user AND rolsuper`,
78
+ params: [],
79
+ },
80
+ ],
81
+ parsePrivileges: (results) => new Set(results.flat().map((r) => String(r['privilege']).toUpperCase())),
82
+ grantTarget: (table, q) => qualify(table, q),
83
+ grantSelectColumns: (target, columns, role, q) => `GRANT SELECT (${columns.map(q).join(', ')}) ON ${target} TO ${role};`,
84
+ renderRole: (role, q) => renderRoleWith(role, q),
85
+ grantPreamble: (table, writer, reader, q) => table.schema === undefined
86
+ ? []
87
+ : [`GRANT USAGE ON SCHEMA ${q(table.schema)} TO ${writer}, ${reader};`],
88
+ });
89
+ export { PLACEHOLDER_ROLE };
90
+ //# sourceMappingURL=postgres.js.map