@nuxodin/item 0.6.5 → 0.6.8
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 +1 -1
- package/tools/schema/db/mysql/from-db.js +3 -3
- package/tools/schema/db/mysql/from-field.js +10 -1
- package/tools/schema/db/mysql/to-db.js +12 -25
- package/tools/schema/db/pg/from-field.js +6 -9
- package/tools/schema/db/pg/to-db.js +3 -14
- package/tools/schema/db/shared/needs-ddl.js +28 -0
- package/tools/schema/db/sqlite/from-db.js +10 -6
- package/tools/schema/db/sqlite/from-field.js +19 -12
- package/tools/schema/db/sqlite/to-db.js +30 -11
- package/tools/schema/db/sqlite/to-field.js +5 -3
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// mysql/from-db.js
|
|
2
2
|
import { schemaFromDb as _schemaFromDb } from '../shared/from-db.js'
|
|
3
|
-
import { quoteId } from '../shared/sql.js'
|
|
3
|
+
import { queryRows, quoteId } from '../shared/sql.js'
|
|
4
4
|
import { schemaFromField } from './from-field.js'
|
|
5
5
|
|
|
6
6
|
const dialect = {
|
|
7
|
-
tables: async (query) => (await query
|
|
8
|
-
fields: async (query, table) => query
|
|
7
|
+
tables: async (query) => (await queryRows(query, 'SHOW TABLES')).map(r => Object.values(r)[0]),
|
|
8
|
+
fields: async (query, table) => queryRows(query, `SHOW FULL FIELDS FROM ${quoteId(table)}`),
|
|
9
9
|
fromField: (row) => ({
|
|
10
10
|
name: row.Field,
|
|
11
11
|
prop: schemaFromField(row),
|
|
@@ -90,7 +90,7 @@ export function schemaFromField(row) {
|
|
|
90
90
|
};
|
|
91
91
|
} else prop.type = "string";
|
|
92
92
|
|
|
93
|
-
if (row.Default != null) prop.default = row.Default;
|
|
93
|
+
if (row.Default != null) prop.default = parseDefault(row.Default, prop.type);
|
|
94
94
|
if (row.Comment) prop["$comment"] = row.Comment;
|
|
95
95
|
if (row.Key === "PRI") prop["x-index"] = "primary";
|
|
96
96
|
if (row.Key === "UNI") prop["x-index"] = "unique";
|
|
@@ -99,3 +99,12 @@ export function schemaFromField(row) {
|
|
|
99
99
|
|
|
100
100
|
return prop;
|
|
101
101
|
}
|
|
102
|
+
|
|
103
|
+
/** SHOW FULL FIELDS hands the default back as text — read it as the value the schema would declare. */
|
|
104
|
+
function parseDefault(value, type) {
|
|
105
|
+
const v = String(value);
|
|
106
|
+
const n = Number(v);
|
|
107
|
+
if (type === "boolean") return v !== "0";
|
|
108
|
+
if ((type === "integer" || type === "number") && !isNaN(n)) return n;
|
|
109
|
+
return v;
|
|
110
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { schemaFromDb } from "./from-db.js";
|
|
3
3
|
import { quoteId, toFieldDef } from "./to-field.js";
|
|
4
4
|
import { schemaDiff } from "../../diff.js";
|
|
5
|
+
import { fieldNeedsDdl } from "../shared/needs-ddl.js";
|
|
5
6
|
|
|
6
7
|
function tableData(tableSchema) {
|
|
7
8
|
const row = tableSchema.additionalProperties ?? {};
|
|
@@ -14,26 +15,10 @@ function primaryFields(fields) {
|
|
|
14
15
|
return fields.filter(([, f]) => f["x-index"] === "primary").map(([n]) => n);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const columnProps = new Set([
|
|
18
|
-
"type", "format", "contentEncoding", "maxLength", "minLength",
|
|
19
|
-
"minimum", "maximum", "multipleOf", "$comment",
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
18
|
function hasOwn(obj, key) {
|
|
23
19
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
function fieldChanged(diffs, table, field, prop) {
|
|
27
|
-
return diffs.some(d =>
|
|
28
|
-
d.path[0] === "properties" &&
|
|
29
|
-
d.path[1] === table &&
|
|
30
|
-
d.path[2] === "additionalProperties" &&
|
|
31
|
-
d.path[3] === "properties" &&
|
|
32
|
-
d.path[4] === field &&
|
|
33
|
-
(columnProps.has(d.property) || d.property === "default" && hasOwn(prop, "default"))
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
22
|
function preserveDefault(prop, currProp) {
|
|
38
23
|
return hasOwn(prop, "default") || !hasOwn(currProp, "default")
|
|
39
24
|
? prop
|
|
@@ -153,7 +138,8 @@ export async function schemaToDb(
|
|
|
153
138
|
const { fields: currFieldEntries, required: currRequired } = tableData(
|
|
154
139
|
current.properties[table],
|
|
155
140
|
);
|
|
156
|
-
const currFields =
|
|
141
|
+
const currFields = new Map(currFieldEntries);
|
|
142
|
+
const nextFieldNames = new Set(fields.map(([n]) => n));
|
|
157
143
|
const indexStmts = await indexStatements(
|
|
158
144
|
query,
|
|
159
145
|
table,
|
|
@@ -165,20 +151,21 @@ export async function schemaToDb(
|
|
|
165
151
|
const autoAfter = new Map();
|
|
166
152
|
for (const [name, prop] of fields) {
|
|
167
153
|
const isRequired = required.has(name);
|
|
168
|
-
const
|
|
169
|
-
const propWithDefault =
|
|
154
|
+
const currProp = currFields.get(name);
|
|
155
|
+
const propWithDefault = currProp ? preserveDefault(prop, currProp) : prop;
|
|
170
156
|
const fieldProp = propWithDefault["x-autoincrement"] ? { ...propWithDefault, "x-autoincrement": false } : propWithDefault;
|
|
171
157
|
const fieldDef = toFieldDef(name, fieldProp, { required: isRequired });
|
|
172
|
-
if (!
|
|
158
|
+
if (!currProp) {
|
|
173
159
|
stmts.push(`ALTER TABLE ${tableId} ADD COLUMN ${fieldDef};`);
|
|
174
160
|
if (prop["x-autoincrement"]) autoAfter.set(name, prop);
|
|
175
161
|
} else {
|
|
176
|
-
const hasAuto = !!
|
|
162
|
+
const hasAuto = !!currProp["x-autoincrement"];
|
|
177
163
|
const wantsAuto = !!prop["x-autoincrement"];
|
|
178
|
-
|
|
164
|
+
// AUTO_INCREMENT is added and dropped by its own statements below, so it stays out of here
|
|
165
|
+
const wantsChange = fieldNeedsDdl(fieldProp, { ...currProp, "x-autoincrement": false }, ["multipleOf", "$comment"]) ||
|
|
179
166
|
isRequired !== currRequired.has(name) ||
|
|
180
167
|
hasAuto && !wantsAuto;
|
|
181
|
-
const changed = wantsChange && fieldDef !== toFieldDef(name,
|
|
168
|
+
const changed = wantsChange && fieldDef !== toFieldDef(name, currProp, { required: currRequired.has(name) });
|
|
182
169
|
if (changed) {
|
|
183
170
|
stmts.push(`ALTER TABLE ${tableId} MODIFY COLUMN ${fieldDef};`);
|
|
184
171
|
}
|
|
@@ -186,8 +173,8 @@ export async function schemaToDb(
|
|
|
186
173
|
}
|
|
187
174
|
}
|
|
188
175
|
if (!patch) {
|
|
189
|
-
for (const name of currFields) {
|
|
190
|
-
if (!
|
|
176
|
+
for (const name of currFields.keys()) {
|
|
177
|
+
if (!nextFieldNames.has(name)) {
|
|
191
178
|
stmts.push(`ALTER TABLE ${tableId} DROP COLUMN ${quoteId(name)};`);
|
|
192
179
|
}
|
|
193
180
|
}
|
|
@@ -46,7 +46,7 @@ export function schemaFromField(row) {
|
|
|
46
46
|
} else if (type === 'json' || type === 'jsonb') prop.type = 'object'
|
|
47
47
|
else prop.type = 'string'
|
|
48
48
|
|
|
49
|
-
const def =
|
|
49
|
+
const def = parseDefault(row.column_default, prop.type)
|
|
50
50
|
if (def !== undefined) prop.default = def
|
|
51
51
|
if (row.comment) prop['$comment'] = row.comment
|
|
52
52
|
if (row.is_primary) prop['x-index'] = 'primary'
|
|
@@ -56,16 +56,13 @@ export function schemaFromField(row) {
|
|
|
56
56
|
return prop
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
/** information_schema hands the default back as an SQL literal — read it as the value the schema would declare. */
|
|
60
|
+
function parseDefault(value, type) {
|
|
60
61
|
if (value == null || /^nextval\(/i.test(value) || /\b(current_|now\()/i.test(value)) return undefined
|
|
61
62
|
let v = String(value).replace(/::[\w\s.[\]"]+$/i, '')
|
|
62
63
|
if (/^'.*'$/.test(v)) v = v.slice(1, -1).replace(/''/g, "'")
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
if (
|
|
64
|
+
const n = Number(v)
|
|
65
|
+
if (type === 'boolean') return /^(true|t|1)$/i.test(v)
|
|
66
|
+
if ((type === 'integer' || type === 'number') && !isNaN(n)) return n
|
|
66
67
|
return v
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
-
function numberOr(fallback, value) {
|
|
70
|
-
return Number.isNaN(value) ? fallback : value
|
|
71
|
-
}
|
|
@@ -3,6 +3,7 @@ import { schemaFromDb } from './from-db.js'
|
|
|
3
3
|
import { queryRows } from '../shared/sql.js'
|
|
4
4
|
import { defaultLiteral, quoteId, quoteLit, toFieldDef, typeSql } from './to-field.js'
|
|
5
5
|
import { schemaDiff } from '../../diff.js'
|
|
6
|
+
import { fieldNeedsDdl } from '../shared/needs-ddl.js'
|
|
6
7
|
|
|
7
8
|
function tableData(tableSchema) {
|
|
8
9
|
const row = tableSchema.additionalProperties ?? {}
|
|
@@ -15,11 +16,6 @@ function primaryFields(fields) {
|
|
|
15
16
|
return fields.filter(([, f]) => f['x-index'] === 'primary').map(([n]) => n)
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const columnProps = new Set([
|
|
19
|
-
'type', 'format', 'contentEncoding', 'maxLength', 'minLength',
|
|
20
|
-
'minimum', 'maximum', 'multipleOf',
|
|
21
|
-
])
|
|
22
|
-
|
|
23
19
|
function hasOwn(obj, key) {
|
|
24
20
|
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
25
21
|
}
|
|
@@ -30,14 +26,6 @@ function setEqual(a, b) {
|
|
|
30
26
|
return true
|
|
31
27
|
}
|
|
32
28
|
|
|
33
|
-
function fieldChanged(diffs, table, field, prop) {
|
|
34
|
-
return diffs.some(d => {
|
|
35
|
-
const [a, b, c, e, f] = d.path
|
|
36
|
-
return a === 'properties' && b === table && c === 'additionalProperties' && e === 'properties' && f === field &&
|
|
37
|
-
(columnProps.has(d.property) || d.property === 'default' && hasOwn(prop, 'default') || d.property === 'x-autoincrement')
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
|
|
41
29
|
function preserveDefault(prop, currProp) {
|
|
42
30
|
return hasOwn(prop, 'default') || !hasOwn(currProp, 'default')
|
|
43
31
|
? prop
|
|
@@ -163,7 +151,8 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
163
151
|
continue
|
|
164
152
|
}
|
|
165
153
|
const propWithDefault = preserveDefault(prop, currProp)
|
|
166
|
-
const changed =
|
|
154
|
+
const changed = fieldNeedsDdl(propWithDefault, currProp, ['multipleOf', '$comment']) ||
|
|
155
|
+
isRequired !== currRequired.has(name)
|
|
167
156
|
if (changed) stmts.push(...alterFieldStmts(table, name, propWithDefault, currProp, isRequired, currRequired.has(name), changed))
|
|
168
157
|
}
|
|
169
158
|
if (!patch) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// shared/needs-ddl.js — does the existing column still satisfy the schema?
|
|
2
|
+
//
|
|
3
|
+
// The schema travels with the database and holds what no DDL expresses (pattern, enum, required,
|
|
4
|
+
// x-*). Reconstructing a schema from the database is lossy, so comparing the two for equality
|
|
5
|
+
// turns every gap into a change and rebuilds every table forever. Each property gets the question
|
|
6
|
+
// that fits it: capacity asks whether the column still holds what the schema wants (widen only),
|
|
7
|
+
// structural asks for equality, everything else is enforced by the application alone.
|
|
8
|
+
import { schemaType } from './sql.js'
|
|
9
|
+
|
|
10
|
+
const capacity = {
|
|
11
|
+
maximum: (need, have) => have >= need,
|
|
12
|
+
minimum: (need, have) => have <= need,
|
|
13
|
+
maxLength: (need, have) => have >= need,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const structural = ['format', 'contentEncoding', 'x-index', 'default']
|
|
17
|
+
|
|
18
|
+
/** @param dialectStructural properties this dialect renders too, e.g. 'multipleOf' on MySQL. */
|
|
19
|
+
export function fieldNeedsDdl(next, curr, dialectStructural = []) {
|
|
20
|
+
for (const [prop, holds] of Object.entries(capacity)) {
|
|
21
|
+
const need = next[prop], have = curr[prop]
|
|
22
|
+
// no requirement, or a bound the database never reported — nothing to widen towards
|
|
23
|
+
if (need !== undefined && have !== undefined && !holds(need, have)) return true
|
|
24
|
+
}
|
|
25
|
+
if (schemaType(next) !== schemaType(curr)) return true
|
|
26
|
+
if (!!next['x-autoincrement'] !== !!curr['x-autoincrement']) return true
|
|
27
|
+
return [...structural, ...dialectStructural].some(prop => next[prop] !== curr[prop])
|
|
28
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// sqlite/from-db.js
|
|
2
2
|
import { schemaFromDb as _schemaFromDb } from '../shared/from-db.js'
|
|
3
|
-
import { quoteId } from '../shared/sql.js'
|
|
3
|
+
import { queryRows, quoteId } from '../shared/sql.js'
|
|
4
4
|
import { schemaFromField } from './from-field.js'
|
|
5
5
|
|
|
6
6
|
/** Single-column, non-constraint indexes: [{ name, column, unique }]. */
|
|
7
7
|
export async function singleColumnIndexes(query, table) {
|
|
8
8
|
const out = []
|
|
9
|
-
for (const row of await query
|
|
9
|
+
for (const row of await queryRows(query, `PRAGMA index_list(${quoteId(table)})`)) {
|
|
10
10
|
if (row.origin && row.origin !== 'c') continue
|
|
11
|
-
const cols = await query
|
|
11
|
+
const cols = await queryRows(query, `PRAGMA index_info(${quoteId(row.name)})`)
|
|
12
12
|
if (cols.length === 1) out.push({ name: row.name, column: cols[0].name, unique: !!row.unique })
|
|
13
13
|
}
|
|
14
14
|
return out
|
|
@@ -22,11 +22,15 @@ async function indexes(query, table) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const dialect = {
|
|
25
|
-
tables: async (query) => (await query
|
|
25
|
+
tables: async (query) => (await queryRows(query, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")).map(r => r.name),
|
|
26
26
|
fields: async (query, table) => {
|
|
27
27
|
const ix = await indexes(query, table)
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// AUTOINCREMENT survives only in the stored DDL — no PRAGMA reports it
|
|
29
|
+
const [master] = await queryRows(query, `SELECT sql FROM sqlite_master WHERE type='table' AND name='${table.replaceAll("'", "''")}'`)
|
|
30
|
+
const auto = /\bautoincrement\b/i.test(master?.sql ?? '')
|
|
31
|
+
return (await queryRows(query, `PRAGMA table_info(${quoteId(table)})`)).map(row =>
|
|
32
|
+
row.pk ? { ...row, autoincrement: auto }
|
|
33
|
+
: ix.has(row.name) ? { ...row, index: ix.get(row.name) } : row
|
|
30
34
|
)
|
|
31
35
|
},
|
|
32
36
|
fromField: (row) => ({
|
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
// PRAGMA table_info returns: cid, name, type, notnull, dflt_value, pk
|
|
3
3
|
// SQLite lengths are in characters — no byte conversion needed
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
integer: { signed: [-2147483648, 2147483647] },
|
|
11
|
-
bigint: { signed: [-9007199254740991, 9007199254740991] }, // JS safe
|
|
12
|
-
}
|
|
5
|
+
// SQLite does not enforce a declared integer width — a TINYINT column happily stores 5000.
|
|
6
|
+
// So every spelling reports the same, real bound: the INTEGER storage class, capped where a
|
|
7
|
+
// JS number stops being exact.
|
|
8
|
+
const intTypes = new Set(['tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint'])
|
|
9
|
+
const intMax = 9007199254740991
|
|
13
10
|
|
|
14
11
|
export function schemaFromField(row) {
|
|
15
12
|
const prop = {}
|
|
@@ -18,9 +15,8 @@ export function schemaFromField(row) {
|
|
|
18
15
|
const type = m?.[1]?.toLowerCase() ?? 'text'
|
|
19
16
|
const len = m?.[2] ? parseInt(m[2]) : null
|
|
20
17
|
|
|
21
|
-
if (type
|
|
22
|
-
|
|
23
|
-
prop.type = 'integer'; prop.minimum = min; prop.maximum = max
|
|
18
|
+
if (intTypes.has(type)) {
|
|
19
|
+
prop.type = 'integer'; prop.minimum = -intMax; prop.maximum = intMax
|
|
24
20
|
|
|
25
21
|
} else if (type === 'boolean' || type === 'bool') { prop.type = 'boolean'
|
|
26
22
|
} else if (type === 'real' || type === 'float' || type === 'double') { prop.type = 'number'
|
|
@@ -41,9 +37,20 @@ export function schemaFromField(row) {
|
|
|
41
37
|
prop.type = 'string'
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
if (row.dflt_value != null) prop.default = row.dflt_value
|
|
40
|
+
if (row.dflt_value != null) prop.default = parseDefault(row.dflt_value, prop.type)
|
|
45
41
|
if (row.pk) prop['x-index'] = 'primary'
|
|
46
42
|
else if (row.index) prop['x-index'] = row.index
|
|
43
|
+
if (row.autoincrement) prop['x-autoincrement'] = true
|
|
47
44
|
|
|
48
45
|
return prop
|
|
49
46
|
}
|
|
47
|
+
|
|
48
|
+
/** PRAGMA hands back the raw SQL literal — read it as the value the schema would declare. */
|
|
49
|
+
function parseDefault(raw, type) {
|
|
50
|
+
const v = String(raw).trim()
|
|
51
|
+
const n = Number(v)
|
|
52
|
+
if (/^'[\s\S]*'$/.test(v)) return v.slice(1, -1).replaceAll("''", "'")
|
|
53
|
+
if (type === 'boolean') return v !== '0'
|
|
54
|
+
if ((type === 'integer' || type === 'number') && !isNaN(n)) return n
|
|
55
|
+
return v
|
|
56
|
+
}
|
|
@@ -4,6 +4,7 @@ import { schemaFromDb, singleColumnIndexes } from './from-db.js'
|
|
|
4
4
|
import { toFieldDef } from './to-field.js'
|
|
5
5
|
import { quoteId, schemaType } from '../shared/sql.js'
|
|
6
6
|
import { schemaDiff } from '../../diff.js'
|
|
7
|
+
import { fieldNeedsDdl } from '../shared/needs-ddl.js'
|
|
7
8
|
|
|
8
9
|
function tableFields(tableSchema) {
|
|
9
10
|
return Object.entries(tableSchema.additionalProperties?.properties ?? {})
|
|
@@ -14,19 +15,32 @@ const indexName = (table, name) => `idx_${table}_${name}`
|
|
|
14
15
|
// SQLite: a single integer primary key must be declared inline as
|
|
15
16
|
// `INTEGER PRIMARY KEY [AUTOINCREMENT]`; AUTOINCREMENT is invalid elsewhere.
|
|
16
17
|
// Composite or non-integer keys use a table-level PRIMARY KEY constraint.
|
|
18
|
+
function soloIntPrimary(fields, primaries) {
|
|
19
|
+
if (primaries.length !== 1) return
|
|
20
|
+
const prop = fields.find(([n]) => n === primaries[0])?.[1]
|
|
21
|
+
return prop && ['integer', 'boolean'].includes(schemaType(prop)) ? primaries[0] : undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// What SQLite never emits cannot be a difference either: a fulltext index needs an FTS virtual
|
|
25
|
+
// table, and AUTOINCREMENT exists only on the rowid alias.
|
|
26
|
+
function emittable(prop, isSoloPk) {
|
|
27
|
+
const out = { ...prop }
|
|
28
|
+
if (out['x-index'] === 'fulltext') delete out['x-index']
|
|
29
|
+
if (!isSoloPk) delete out['x-autoincrement']
|
|
30
|
+
return out
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
function createBody(fields, primaries) {
|
|
18
|
-
const
|
|
19
|
-
const soloInt = primaries.length === 1 && pkProp &&
|
|
20
|
-
['integer', 'boolean'].includes(schemaType(pkProp))
|
|
34
|
+
const solo = soloIntPrimary(fields, primaries)
|
|
21
35
|
const cols = fields.map(([n, f]) => {
|
|
22
|
-
const isPk =
|
|
36
|
+
const isPk = n === solo
|
|
23
37
|
// Only `INTEGER PRIMARY KEY` is the rowid alias, and AUTOINCREMENT is legal nowhere else —
|
|
24
38
|
// so a boolean key keeps the integer spelling, which SQLite would otherwise reject.
|
|
25
39
|
let def = toFieldDef(n, isPk ? { ...f, type: 'integer' } : f)
|
|
26
40
|
if (isPk) def += f['x-autoincrement'] ? ' PRIMARY KEY AUTOINCREMENT' : ' PRIMARY KEY'
|
|
27
41
|
return ' ' + def
|
|
28
42
|
}).join(',\n')
|
|
29
|
-
const pk = (!
|
|
43
|
+
const pk = (!solo && primaries.length) ? `,\n PRIMARY KEY (${primaries.map(quoteId).join(', ')})` : ''
|
|
30
44
|
return `${cols}${pk}`
|
|
31
45
|
}
|
|
32
46
|
|
|
@@ -88,13 +102,18 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
88
102
|
stmts.push(`CREATE TABLE ${quoteId(table)} (\n${createBody(fields, primaries)}\n);`)
|
|
89
103
|
stmts.push(...indexStatements(table, fields, primaries))
|
|
90
104
|
} else {
|
|
91
|
-
const currFields =
|
|
92
|
-
const
|
|
93
|
-
|
|
105
|
+
const currFields = new Map(tableFields(current.properties[table]))
|
|
106
|
+
const nextFieldNames = new Set(fields.map(([n]) => n))
|
|
107
|
+
// A new plain column is an ADD COLUMN; only a new key column needs the rebuild.
|
|
108
|
+
const solo = soloIntPrimary(fields, primaries)
|
|
109
|
+
const hasChanges = fields.some(([name, prop]) => currFields.has(name)
|
|
110
|
+
? fieldNeedsDdl(emittable(prop, name === solo), currFields.get(name))
|
|
111
|
+
: prop['x-index'] === 'primary' || prop['x-autoincrement'])
|
|
112
|
+
const hasDrops = !patch && [...currFields.keys()].some(n => !nextFieldNames.has(n))
|
|
94
113
|
|
|
95
|
-
if (hasChanges &&
|
|
114
|
+
if (hasDrops || hasChanges && !patch) {
|
|
96
115
|
const tmp = `${table}_migration_tmp`
|
|
97
|
-
const keep = fields.map(([n]) => n).filter(n => currFields.
|
|
116
|
+
const keep = fields.map(([n]) => n).filter(n => currFields.has(n))
|
|
98
117
|
const colsList = keep.map(quoteId).join(', ')
|
|
99
118
|
|
|
100
119
|
stmts.push(`CREATE TABLE ${quoteId(tmp)} (\n${createBody(fields, primaries)}\n);`)
|
|
@@ -104,7 +123,7 @@ export async function schemaToDb(schema, query, { force = false, patch = false }
|
|
|
104
123
|
stmts.push(...indexStatements(table, fields, primaries))
|
|
105
124
|
} else {
|
|
106
125
|
for (const [name, prop] of fields) {
|
|
107
|
-
if (!currFields.
|
|
126
|
+
if (!currFields.has(name))
|
|
108
127
|
stmts.push(`ALTER TABLE ${quoteId(table)} ADD COLUMN ${toFieldDef(name, prop)};`)
|
|
109
128
|
}
|
|
110
129
|
stmts.push(...indexStatements(table, fields, primaries, await singleColumnIndexes(query, table), { patch }))
|
|
@@ -17,9 +17,11 @@ export function toFieldDef(name, prop) {
|
|
|
17
17
|
else if (t === 'number') type = 'REAL'
|
|
18
18
|
else if (t === 'object' || t === 'array') type = 'TEXT' // JSON as text
|
|
19
19
|
else {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// Same reasoning as BOOLEAN: the declared name is the only carrier of the format, and
|
|
21
|
+
// schemaFromField reads these back. TEXT would round-trip to a plain string.
|
|
22
|
+
if (prop.format === 'date') type = 'DATE'
|
|
23
|
+
else if (prop.format === 'time') type = 'TIME'
|
|
24
|
+
else if (prop.format === 'date-time') type = 'DATETIME'
|
|
23
25
|
else if (prop.contentEncoding === 'base64') type = 'BLOB'
|
|
24
26
|
else if (prop.maxLength) type = `VARCHAR(${prop.maxLength})`
|
|
25
27
|
else type = 'TEXT'
|