@crombolo/data 0.9.1 → 0.10.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/package.json +2 -2
- package/src/bundle-tables/builder.ts +264 -0
- package/src/bundle-tables/capability.ts +377 -0
- package/src/bundle-tables/chain.ts +105 -0
- package/src/bundle-tables/ddl.ts +124 -0
- package/src/bundle-tables/guard.ts +116 -0
- package/src/bundle-tables/index.ts +7 -0
- package/src/bundle-tables/names.ts +58 -0
- package/src/bundle-tables/schema.ts +485 -0
- package/src/cluster.ts +1 -1
- package/src/dialect.ts +8 -0
- package/src/index.ts +3 -0
- package/src/migrations.ts +147 -31
- package/src/pending.ts +1 -1
- package/src/repositories/bundles.ts +36 -4
- package/src/repositories/documents.ts +1 -1
- package/src/repositories/form-entries.ts +1 -1
- package/src/repositories/form-workflows.ts +1 -1
- package/src/repositories/transfer-runs.ts +20 -2
- package/src/schema/bundle-components.ts +1 -1
- package/src/schema/bundle-package.ts +27 -0
- package/src/schema/bundles-rename.ts +1 -1
- package/src/schema/content.ts +2 -2
- package/src/schema/created-packages.ts +1 -1
- package/src/schema/form-permissions.ts +1 -1
- package/src/schema/form-workflows.ts +1 -1
- package/src/schema/forms.ts +1 -1
- package/src/schema/identity.ts +1 -1
- package/src/schema/plan.ts +4 -0
- package/src/schema/transfer-run-package.ts +27 -0
- package/src/schema/upgrade-report.ts +1 -1
- package/src/schema-state.ts +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bundle's compiled steps as a migration chain, run by the same `migrate`,
|
|
3
|
+
* under the same lock and into the same ledger as core's — only its state key
|
|
4
|
+
* and its ledger rows' `bundle` differ. And purging one: dropping exactly the
|
|
5
|
+
* tables under its prefix, found in the database rather than the declaration,
|
|
6
|
+
* because a purged bundle is one the site no longer wires.
|
|
7
|
+
*/
|
|
8
|
+
import type { Db } from '../database.ts'
|
|
9
|
+
import { deleteKeyValue, type Migration, MigrationPlan, recordInLedger } from '../migrations.ts'
|
|
10
|
+
import { createTablesCapability } from './capability.ts'
|
|
11
|
+
import { renderStep } from './ddl.ts'
|
|
12
|
+
import { guardStatement } from './guard.ts'
|
|
13
|
+
import { bundlePrefix } from './names.ts'
|
|
14
|
+
import type { CompiledStep, CompiledTables } from './schema.ts'
|
|
15
|
+
|
|
16
|
+
function migrationOf(step: CompiledStep): Migration {
|
|
17
|
+
return {
|
|
18
|
+
from: step.from,
|
|
19
|
+
to: step.to,
|
|
20
|
+
name: step.name,
|
|
21
|
+
kind: step.kind,
|
|
22
|
+
release: step.release,
|
|
23
|
+
contracts: step.contracts,
|
|
24
|
+
checksum: step.checksum,
|
|
25
|
+
async up(db) {
|
|
26
|
+
for (const sql of renderStep(db.dialect, step)) await db.exec(sql)
|
|
27
|
+
},
|
|
28
|
+
backfill: step.backfill
|
|
29
|
+
? (db) =>
|
|
30
|
+
(step.backfill as NonNullable<CompiledStep['backfill']>)(
|
|
31
|
+
createTablesCapability(db, step.after),
|
|
32
|
+
)
|
|
33
|
+
: undefined,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The chain `migrate` walks. Throws when the declaration has problems: compile first. */
|
|
38
|
+
export function bundlePlan(compiled: CompiledTables): MigrationPlan {
|
|
39
|
+
if (compiled.problems.length > 0)
|
|
40
|
+
throw new Error(
|
|
41
|
+
`The bundle "${compiled.id}"'s tables cannot be planned:\n- ${compiled.problems.join('\n- ')}`,
|
|
42
|
+
)
|
|
43
|
+
return new MigrationPlan(compiled.steps.map(migrationOf), {
|
|
44
|
+
stateKey: compiled.stateKey,
|
|
45
|
+
initialState: compiled.initialState,
|
|
46
|
+
bundle: compiled.id,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a bundle's tables directly, step by step, without its ledger or its
|
|
52
|
+
* state. Only for proving what core's own migrations do around a bundle's
|
|
53
|
+
* tables: an install is always `migrate`.
|
|
54
|
+
*/
|
|
55
|
+
export async function applyBundleStepsUnledgered(db: Db, compiled: CompiledTables): Promise<void> {
|
|
56
|
+
for (const migration of bundlePlan(compiled).migrations)
|
|
57
|
+
await db.transaction(async (tx) => {
|
|
58
|
+
await migration.up(tx)
|
|
59
|
+
await migration.backfill?.(tx)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Every table in the database under a bundle's prefix. */
|
|
64
|
+
export async function bundleTablesIn(db: Db, id: string): Promise<string[]> {
|
|
65
|
+
const prefix = bundlePrefix(id)
|
|
66
|
+
const rows =
|
|
67
|
+
db.dialect.name === 'sqlite'
|
|
68
|
+
? await db.query<{ name: string }>("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
69
|
+
: await db.query<{ name: string }>(
|
|
70
|
+
"SELECT table_name AS name FROM information_schema.tables WHERE table_schema = current_schema() AND table_type = 'BASE TABLE'",
|
|
71
|
+
)
|
|
72
|
+
// Compared as text rather than with LIKE, where `_` is a wildcard.
|
|
73
|
+
return rows
|
|
74
|
+
.map((row) => row.name)
|
|
75
|
+
.filter((name) => name.startsWith(prefix))
|
|
76
|
+
.sort()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Drops every table under the bundle's prefix and forgets its state, so adding
|
|
81
|
+
* it again starts its chain from the beginning. Ledgered as a `purge`.
|
|
82
|
+
*/
|
|
83
|
+
export async function purgeBundleTables(
|
|
84
|
+
db: Db,
|
|
85
|
+
id: string,
|
|
86
|
+
options: { by?: string; stateKey: string },
|
|
87
|
+
): Promise<string[]> {
|
|
88
|
+
const prefix = bundlePrefix(id)
|
|
89
|
+
const tables = await bundleTablesIn(db, id)
|
|
90
|
+
const started = performance.now()
|
|
91
|
+
await db.transaction(async (tx) => {
|
|
92
|
+
for (const table of tables)
|
|
93
|
+
await tx.exec(guardStatement(id, prefix, `DROP TABLE ${tx.dialect.quote(table)}`))
|
|
94
|
+
})
|
|
95
|
+
await deleteKeyValue(db, options.stateKey)
|
|
96
|
+
await recordInLedger(db, {
|
|
97
|
+
name: `purge ${id}`,
|
|
98
|
+
kind: 'purge',
|
|
99
|
+
durationMs: Math.round(performance.now() - started),
|
|
100
|
+
appliedBy: options.by ?? null,
|
|
101
|
+
note: tables.length > 0 ? `dropped ${tables.join(', ')}` : 'no tables',
|
|
102
|
+
bundle: id,
|
|
103
|
+
})
|
|
104
|
+
return tables
|
|
105
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A step's operations as SQL, per dialect. Every identifier comes from the
|
|
3
|
+
* resolved schema and is quoted here, every default is a literal the builder
|
|
4
|
+
* already checked, and every statement passes the guard before it is returned.
|
|
5
|
+
*/
|
|
6
|
+
import type { Dialect } from '../dialect.ts'
|
|
7
|
+
import type { ColumnSpec, Operation } from './builder.ts'
|
|
8
|
+
import { guardStatement } from './guard.ts'
|
|
9
|
+
import type { ResolvedSchema } from './schema.ts'
|
|
10
|
+
|
|
11
|
+
/** The column type, from the dialect seam. Text compares case-sensitively on both. */
|
|
12
|
+
export function columnType(dialect: Dialect, spec: ColumnSpec): string {
|
|
13
|
+
const t = dialect.types
|
|
14
|
+
switch (spec.type) {
|
|
15
|
+
case 'uuid':
|
|
16
|
+
return t.uuid
|
|
17
|
+
case 'varchar':
|
|
18
|
+
return t.varcharExact(spec.length ?? 1)
|
|
19
|
+
case 'text':
|
|
20
|
+
return t.text
|
|
21
|
+
case 'integer':
|
|
22
|
+
return t.integer
|
|
23
|
+
case 'bigint':
|
|
24
|
+
return t.bigint
|
|
25
|
+
case 'boolean':
|
|
26
|
+
return t.boolean
|
|
27
|
+
case 'timestamp':
|
|
28
|
+
return t.timestamp
|
|
29
|
+
case 'json':
|
|
30
|
+
return t.json
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function literal(dialect: Dialect, value: string | number | boolean): string {
|
|
35
|
+
if (typeof value === 'boolean')
|
|
36
|
+
return dialect.name === 'postgres' ? (value ? 'TRUE' : 'FALSE') : value ? '1' : '0'
|
|
37
|
+
if (typeof value === 'number') return String(value)
|
|
38
|
+
return `'${value.replaceAll("'", "''")}'`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function columnDefinition(dialect: Dialect, name: string, spec: ColumnSpec): string {
|
|
42
|
+
const parts = [dialect.quote(name), columnType(dialect, spec)]
|
|
43
|
+
if (!spec.nullable) parts.push('NOT NULL')
|
|
44
|
+
if (spec.default !== undefined) parts.push(`DEFAULT ${literal(dialect, spec.default)}`)
|
|
45
|
+
return parts.join(' ')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The statements one operation runs, against the schema as it stood before it. */
|
|
49
|
+
export function renderOperation(
|
|
50
|
+
dialect: Dialect,
|
|
51
|
+
before: ResolvedSchema,
|
|
52
|
+
after: ResolvedSchema,
|
|
53
|
+
op: Operation,
|
|
54
|
+
): string[] {
|
|
55
|
+
const q = (identifier: string) => dialect.quote(identifier)
|
|
56
|
+
const guard = (sql: string) => guardStatement(before.id, before.prefix, sql)
|
|
57
|
+
const table = after.tables.get(op.table) ?? before.tables.get(op.table)
|
|
58
|
+
if (!table) throw new Error(`no table "${op.table}" to render ${op.op} against`)
|
|
59
|
+
const statements: string[] = []
|
|
60
|
+
const indexFor = (columns: readonly string[]) => {
|
|
61
|
+
const index = (after.tables.get(op.table) ?? table).indexes.get(columns.join(','))
|
|
62
|
+
if (!index) throw new Error(`no index on "${op.table}" (${columns.join(',')})`)
|
|
63
|
+
return index
|
|
64
|
+
}
|
|
65
|
+
const createIndex = (columns: readonly string[]) => {
|
|
66
|
+
const index = indexFor(columns)
|
|
67
|
+
statements.push(
|
|
68
|
+
`CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${q(index.name)} ON ${q(table.physical)} (${index.columns.map(q).join(', ')})`,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
switch (op.op) {
|
|
73
|
+
case 'createTable': {
|
|
74
|
+
const columns = Object.entries(op.columns).map(([name, spec]) =>
|
|
75
|
+
columnDefinition(dialect, name, spec),
|
|
76
|
+
)
|
|
77
|
+
const key = Object.entries(op.columns).find(([, spec]) => spec.primaryKey)?.[0] as string
|
|
78
|
+
statements.push(
|
|
79
|
+
`CREATE TABLE ${q(table.physical)} (${columns.join(', ')}, PRIMARY KEY (${q(key)}))`,
|
|
80
|
+
)
|
|
81
|
+
for (const [name, spec] of Object.entries(op.columns))
|
|
82
|
+
if (spec.indexed && !spec.primaryKey) createIndex([name])
|
|
83
|
+
break
|
|
84
|
+
}
|
|
85
|
+
case 'addColumn':
|
|
86
|
+
statements.push(
|
|
87
|
+
`ALTER TABLE ${q(table.physical)} ADD COLUMN ${columnDefinition(dialect, op.column, op.spec)}`,
|
|
88
|
+
)
|
|
89
|
+
if (op.spec.indexed) createIndex([op.column])
|
|
90
|
+
break
|
|
91
|
+
case 'addIndex':
|
|
92
|
+
createIndex(op.columns)
|
|
93
|
+
break
|
|
94
|
+
case 'widenColumn':
|
|
95
|
+
// SQLite stores by affinity and enforces no length, so a wider type is
|
|
96
|
+
// already what it holds; only Postgres has anything to change.
|
|
97
|
+
if (dialect.name === 'postgres')
|
|
98
|
+
statements.push(
|
|
99
|
+
`ALTER TABLE ${q(table.physical)} ALTER COLUMN ${q(op.column)} TYPE ${columnType(dialect, op.spec)}`,
|
|
100
|
+
)
|
|
101
|
+
break
|
|
102
|
+
case 'dropTable':
|
|
103
|
+
statements.push(`DROP TABLE ${q(table.physical)}`)
|
|
104
|
+
break
|
|
105
|
+
case 'dropColumn':
|
|
106
|
+
statements.push(`ALTER TABLE ${q(table.physical)} DROP COLUMN ${q(op.column)}`)
|
|
107
|
+
break
|
|
108
|
+
case 'dropIndex': {
|
|
109
|
+
const index = before.tables.get(op.table)?.indexes.get(op.columns.join(','))
|
|
110
|
+
if (!index) throw new Error(`no index on "${op.table}" (${op.columns.join(',')})`)
|
|
111
|
+
statements.push(`DROP INDEX ${q(index.name)}`)
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return statements.map(guard)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Every statement a step runs, in order. */
|
|
119
|
+
export function renderStep(
|
|
120
|
+
dialect: Dialect,
|
|
121
|
+
step: { before: ResolvedSchema; after: ResolvedSchema; operations: readonly Operation[] },
|
|
122
|
+
): string[] {
|
|
123
|
+
return step.operations.flatMap((op) => renderOperation(dialect, step.before, step.after, op))
|
|
124
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one check every statement a bundle's builder or capability emits passes.
|
|
3
|
+
* `docs/bundles.md`, Part 4.
|
|
4
|
+
*
|
|
5
|
+
* It only ever sees SQL this module wrote, which is regular enough to check
|
|
6
|
+
* token by token: every identifier is double-quoted and lowercase, every value
|
|
7
|
+
* is a `?` placeholder or a literal the builder rendered, and there is exactly
|
|
8
|
+
* one statement. What it asserts is that every table or index the statement
|
|
9
|
+
* names carries the bundle's own prefix. A statement that fails is refused
|
|
10
|
+
* before it reaches the database, so a bug in the code above it is a refusal
|
|
11
|
+
* rather than a query against a core table.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A refusal: the statement never ran. */
|
|
15
|
+
export class BundleStatementError extends Error {
|
|
16
|
+
constructor(
|
|
17
|
+
readonly bundle: string,
|
|
18
|
+
readonly reason: string,
|
|
19
|
+
) {
|
|
20
|
+
super(`Bundle "${bundle}": a statement was refused before it ran: ${reason}.`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Keywords after which the next identifier names a table or an index. */
|
|
25
|
+
const OBJECT_POSITIONS = new Set([
|
|
26
|
+
'FROM',
|
|
27
|
+
'INTO',
|
|
28
|
+
'UPDATE',
|
|
29
|
+
'TABLE',
|
|
30
|
+
'JOIN',
|
|
31
|
+
'INDEX',
|
|
32
|
+
'ON',
|
|
33
|
+
'REFERENCES',
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
/** Keywords that may sit between a position keyword and the object it names. */
|
|
37
|
+
const FILLERS = new Set(['IF', 'NOT', 'EXISTS', 'ONLY'])
|
|
38
|
+
|
|
39
|
+
type Token =
|
|
40
|
+
| { kind: 'word'; value: string }
|
|
41
|
+
| { kind: 'identifier'; value: string }
|
|
42
|
+
| { kind: 'other'; value: string }
|
|
43
|
+
|
|
44
|
+
const QUOTED = /^"([a-z][a-z0-9_]*)"/
|
|
45
|
+
const WORD = /^[A-Za-z_][A-Za-z0-9_]*/
|
|
46
|
+
const STRING = /^'(?:[^']|'')*'/
|
|
47
|
+
const NUMBER = /^-?\d+(?:\.\d+)?/
|
|
48
|
+
const PUNCTUATION = /^(?:<=|>=|<>|[(),=<>*?.])/
|
|
49
|
+
|
|
50
|
+
function tokenize(bundle: string, sql: string): Token[] {
|
|
51
|
+
const tokens: Token[] = []
|
|
52
|
+
let rest = sql
|
|
53
|
+
for (;;) {
|
|
54
|
+
rest = rest.replace(/^\s+/, '')
|
|
55
|
+
if (rest === '') return tokens
|
|
56
|
+
if (rest.startsWith('--') || rest.startsWith('/*'))
|
|
57
|
+
throw new BundleStatementError(bundle, 'it contains a comment')
|
|
58
|
+
if (rest.startsWith(';')) throw new BundleStatementError(bundle, 'it contains a ";"')
|
|
59
|
+
if (rest.startsWith('"')) {
|
|
60
|
+
const match = QUOTED.exec(rest)
|
|
61
|
+
if (!match)
|
|
62
|
+
throw new BundleStatementError(bundle, 'it quotes an identifier no bundle may write')
|
|
63
|
+
tokens.push({ kind: 'identifier', value: match[1] as string })
|
|
64
|
+
rest = rest.slice(match[0].length)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
const string = STRING.exec(rest)
|
|
68
|
+
if (string) {
|
|
69
|
+
tokens.push({ kind: 'other', value: string[0] })
|
|
70
|
+
rest = rest.slice(string[0].length)
|
|
71
|
+
continue
|
|
72
|
+
}
|
|
73
|
+
const word = WORD.exec(rest)
|
|
74
|
+
if (word) {
|
|
75
|
+
tokens.push({ kind: 'word', value: word[0].toUpperCase() })
|
|
76
|
+
rest = rest.slice(word[0].length)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
const other = NUMBER.exec(rest) ?? PUNCTUATION.exec(rest)
|
|
80
|
+
if (!other) throw new BundleStatementError(bundle, `it contains "${rest[0]}"`)
|
|
81
|
+
tokens.push({ kind: 'other', value: other[0] })
|
|
82
|
+
rest = rest.slice(other[0].length)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The tables and indexes a statement names, as the guard sees them. */
|
|
87
|
+
export function objectsNamed(bundle: string, sql: string): string[] {
|
|
88
|
+
const tokens = tokenize(bundle, sql)
|
|
89
|
+
const named: string[] = []
|
|
90
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
91
|
+
const token = tokens[i] as Token
|
|
92
|
+
if (token.kind !== 'word' || !OBJECT_POSITIONS.has(token.value)) continue
|
|
93
|
+
// `ON CONFLICT` and `ON DELETE` are not objects; only an index's `ON <table>` is.
|
|
94
|
+
let j = i + 1
|
|
95
|
+
while (tokens[j]?.kind === 'word' && FILLERS.has((tokens[j] as Token).value)) j++
|
|
96
|
+
const next = tokens[j]
|
|
97
|
+
if (token.value === 'ON' && next?.kind !== 'identifier') continue
|
|
98
|
+
if (next?.kind !== 'identifier')
|
|
99
|
+
throw new BundleStatementError(bundle, `"${token.value}" is not followed by a quoted name`)
|
|
100
|
+
named.push(next.value)
|
|
101
|
+
}
|
|
102
|
+
return named
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Refuses a statement unless every table and index it names carries `prefix`,
|
|
107
|
+
* and it names at least one.
|
|
108
|
+
*/
|
|
109
|
+
export function guardStatement(bundle: string, prefix: string, sql: string): string {
|
|
110
|
+
const named = objectsNamed(bundle, sql)
|
|
111
|
+
if (named.length === 0) throw new BundleStatementError(bundle, 'it names no table')
|
|
112
|
+
for (const name of named)
|
|
113
|
+
if (!name.startsWith(prefix))
|
|
114
|
+
throw new BundleStatementError(bundle, `it names "${name}", which is not this bundle's`)
|
|
115
|
+
return sql
|
|
116
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The names a bundle's tables are stored under. `docs/bundles.md`, Part 4.
|
|
3
|
+
*
|
|
4
|
+
* Every physical name is `bundle_<id>__<name>`, with the id's hyphens written
|
|
5
|
+
* as underscores. An id may not contain `--` and a logical name may not contain
|
|
6
|
+
* `__`, so the double underscore is an unambiguous separator and no two bundles
|
|
7
|
+
* can produce the same physical name. Core reserves the `bundle_` prefix, so a
|
|
8
|
+
* match on a bundle's own prefix can only ever be that bundle's table.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Postgres truncates a longer identifier silently, so a longer one is refused. */
|
|
12
|
+
export const MAX_IDENTIFIER = 63
|
|
13
|
+
|
|
14
|
+
/** Table and column names, as a bundle writes them. */
|
|
15
|
+
export const LOGICAL_NAME = /^[a-z][a-z0-9_]*$/
|
|
16
|
+
|
|
17
|
+
/** The prefix every one of this bundle's objects carries. */
|
|
18
|
+
export function bundlePrefix(id: string): string {
|
|
19
|
+
return `bundle_${id.replaceAll('-', '_')}__`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const physicalTable = (id: string, table: string): string => `${bundlePrefix(id)}${table}`
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An index's name, generated rather than chosen, from what it covers. The
|
|
26
|
+
* `ix_` that follows the prefix is a namespace a table may not enter, because
|
|
27
|
+
* Postgres keeps tables and indexes in one.
|
|
28
|
+
*/
|
|
29
|
+
export function indexName(id: string, table: string, columns: readonly string[]): string {
|
|
30
|
+
const hash = new Bun.CryptoHasher('sha256').update(`${table}(${columns.join(',')})`).digest('hex')
|
|
31
|
+
return `${bundlePrefix(id)}ix_${hash.slice(0, 12)}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Why an id cannot own tables, or undefined when it can. */
|
|
35
|
+
export function bundleIdProblem(id: string): string | undefined {
|
|
36
|
+
if (id.includes('--'))
|
|
37
|
+
return `the id "${id}" contains "--", which would make its table names ambiguous`
|
|
38
|
+
if (id.endsWith('-'))
|
|
39
|
+
return `the id "${id}" ends with "-", which would make its table names ambiguous`
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Why a table or column name is not allowed, or undefined when it is. */
|
|
44
|
+
export function logicalNameProblem(kind: 'table' | 'column', name: string): string | undefined {
|
|
45
|
+
if (typeof name !== 'string' || !LOGICAL_NAME.test(name))
|
|
46
|
+
return `the ${kind} name "${String(name)}" is not lowercase letters, digits and underscores starting with a letter`
|
|
47
|
+
if (name.includes('__')) return `the ${kind} name "${name}" contains "__", the separator`
|
|
48
|
+
if (kind === 'table' && name.startsWith('ix_'))
|
|
49
|
+
return `the table name "${name}" starts with "ix_", which index names use`
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Why a physical name is too long for Postgres, or undefined when it fits. */
|
|
54
|
+
export function lengthProblem(physical: string): string | undefined {
|
|
55
|
+
return physical.length > MAX_IDENTIFIER
|
|
56
|
+
? `"${physical}" is ${physical.length} characters, past the ${MAX_IDENTIFIER} Postgres keeps`
|
|
57
|
+
: undefined
|
|
58
|
+
}
|