Change endpoint from persons to people

This commit is contained in:
xfarrow
2025-03-23 21:00:08 +01:00
parent 4ae263662c
commit d005193f63
7158 changed files with 700476 additions and 735 deletions

View File

@ -0,0 +1,25 @@
const Transaction = require('../../../execution/transaction');
class Transaction_Sqlite extends Transaction {
begin(conn) {
// SQLite doesn't really support isolation levels, it is serializable by
// default and so we override it to ignore isolation level.
// There is a `PRAGMA read_uncommitted = true;`, but that's probably not
// what the user wants
if (this.isolationLevel) {
this.client.logger.warn(
'sqlite3 only supports serializable transactions, ignoring the isolation level param'
);
}
// SQLite infers read vs write transactions from the statement operation
// https://www.sqlite.org/lang_transaction.html#read_transactions_versus_write_transactions
if (this.readOnly) {
this.client.logger.warn(
'sqlite3 implicitly handles read vs write transactions'
);
}
return this.query(conn, 'BEGIN;');
}
}
module.exports = Transaction_Sqlite;

View File

@ -0,0 +1,250 @@
// SQLite3
// -------
const defaults = require('lodash/defaults');
const map = require('lodash/map');
const { promisify } = require('util');
const Client = require('../../client');
const Raw = require('../../raw');
const Transaction = require('./execution/sqlite-transaction');
const SqliteQueryCompiler = require('./query/sqlite-querycompiler');
const SchemaCompiler = require('./schema/sqlite-compiler');
const ColumnCompiler = require('./schema/sqlite-columncompiler');
const TableCompiler = require('./schema/sqlite-tablecompiler');
const ViewCompiler = require('./schema/sqlite-viewcompiler');
const SQLite3_DDL = require('./schema/ddl');
const Formatter = require('../../formatter');
const QueryBuilder = require('./query/sqlite-querybuilder');
class Client_SQLite3 extends Client {
constructor(config) {
super(config);
if (config.connection && config.connection.filename === undefined) {
this.logger.warn(
'Could not find `connection.filename` in config. Please specify ' +
'the database path and name to avoid errors. ' +
'(see docs https://knexjs.org/guide/#configuration-options)'
);
}
if (config.useNullAsDefault === undefined) {
this.logger.warn(
'sqlite does not support inserting default values. Set the ' +
'`useNullAsDefault` flag to hide this warning. ' +
'(see docs https://knexjs.org/guide/query-builder.html#insert).'
);
}
}
_driver() {
return require('sqlite3');
}
schemaCompiler() {
return new SchemaCompiler(this, ...arguments);
}
transaction() {
return new Transaction(this, ...arguments);
}
queryCompiler(builder, formatter) {
return new SqliteQueryCompiler(this, builder, formatter);
}
queryBuilder() {
return new QueryBuilder(this);
}
viewCompiler(builder, formatter) {
return new ViewCompiler(this, builder, formatter);
}
columnCompiler() {
return new ColumnCompiler(this, ...arguments);
}
tableCompiler() {
return new TableCompiler(this, ...arguments);
}
ddl(compiler, pragma, connection) {
return new SQLite3_DDL(this, compiler, pragma, connection);
}
wrapIdentifierImpl(value) {
return value !== '*' ? `\`${value.replace(/`/g, '``')}\`` : '*';
}
// Get a raw connection from the database, returning a promise with the connection object.
acquireRawConnection() {
return new Promise((resolve, reject) => {
// the default mode for sqlite3
let flags = this.driver.OPEN_READWRITE | this.driver.OPEN_CREATE;
if (this.connectionSettings.flags) {
if (!Array.isArray(this.connectionSettings.flags)) {
throw new Error(`flags must be an array of strings`);
}
this.connectionSettings.flags.forEach((_flag) => {
if (!_flag.startsWith('OPEN_') || !this.driver[_flag]) {
throw new Error(`flag ${_flag} not supported by node-sqlite3`);
}
flags = flags | this.driver[_flag];
});
}
const db = new this.driver.Database(
this.connectionSettings.filename,
flags,
(err) => {
if (err) {
return reject(err);
}
resolve(db);
}
);
});
}
// Used to explicitly close a connection, called internally by the pool when
// a connection times out or the pool is shutdown.
async destroyRawConnection(connection) {
const close = promisify((cb) => connection.close(cb));
return close();
}
// Runs the query on the specified connection, providing the bindings and any
// other necessary prep work.
_query(connection, obj) {
if (!obj.sql) throw new Error('The query is empty');
const { method } = obj;
let callMethod;
switch (method) {
case 'insert':
case 'update':
callMethod = obj.returning ? 'all' : 'run';
break;
case 'counter':
case 'del':
callMethod = 'run';
break;
default:
callMethod = 'all';
}
return new Promise(function (resolver, rejecter) {
if (!connection || !connection[callMethod]) {
return rejecter(
new Error(`Error calling ${callMethod} on connection.`)
);
}
connection[callMethod](obj.sql, obj.bindings, function (err, response) {
if (err) return rejecter(err);
obj.response = response;
// We need the context here, as it contains
// the "this.lastID" or "this.changes"
obj.context = this;
return resolver(obj);
});
});
}
_stream(connection, obj, stream) {
if (!obj.sql) throw new Error('The query is empty');
const client = this;
return new Promise(function (resolver, rejecter) {
stream.on('error', rejecter);
stream.on('end', resolver);
return client
._query(connection, obj)
.then((obj) => obj.response)
.then((rows) => rows.forEach((row) => stream.write(row)))
.catch(function (err) {
stream.emit('error', err);
})
.then(function () {
stream.end();
});
});
}
// Ensures the response is returned in the same format as other clients.
processResponse(obj, runner) {
const ctx = obj.context;
const { response, returning } = obj;
if (obj.output) return obj.output.call(runner, response);
switch (obj.method) {
case 'select':
return response;
case 'first':
return response[0];
case 'pluck':
return map(response, obj.pluck);
case 'insert': {
if (returning) {
if (response) {
return response;
}
}
return [ctx.lastID];
}
case 'update': {
if (returning) {
if (response) {
return response;
}
}
return ctx.changes;
}
case 'del':
case 'counter':
return ctx.changes;
default: {
return response;
}
}
}
poolDefaults() {
return defaults({ min: 1, max: 1 }, super.poolDefaults());
}
formatter(builder) {
return new Formatter(this, builder);
}
values(values, builder, formatter) {
if (Array.isArray(values)) {
if (Array.isArray(values[0])) {
return `( values ${values
.map(
(value) =>
`(${this.parameterize(value, undefined, builder, formatter)})`
)
.join(', ')})`;
}
return `(${this.parameterize(values, undefined, builder, formatter)})`;
}
if (values instanceof Raw) {
return `(${this.parameter(values, builder, formatter)})`;
}
return this.parameter(values, builder, formatter);
}
}
Object.assign(Client_SQLite3.prototype, {
dialect: 'sqlite3',
driverName: 'sqlite3',
});
module.exports = Client_SQLite3;

View File

@ -0,0 +1,33 @@
const QueryBuilder = require('../../../query/querybuilder.js');
module.exports = class QueryBuilder_SQLite3 extends QueryBuilder {
withMaterialized(alias, statementOrColumnList, nothingOrStatement) {
this._validateWithArgs(
alias,
statementOrColumnList,
nothingOrStatement,
'with'
);
return this.withWrapped(
alias,
statementOrColumnList,
nothingOrStatement,
true
);
}
withNotMaterialized(alias, statementOrColumnList, nothingOrStatement) {
this._validateWithArgs(
alias,
statementOrColumnList,
nothingOrStatement,
'with'
);
return this.withWrapped(
alias,
statementOrColumnList,
nothingOrStatement,
false
);
}
};

View File

@ -0,0 +1,334 @@
// SQLite3 Query Builder & Compiler
const constant = require('lodash/constant');
const each = require('lodash/each');
const identity = require('lodash/identity');
const isEmpty = require('lodash/isEmpty');
const reduce = require('lodash/reduce');
const QueryCompiler = require('../../../query/querycompiler');
const noop = require('../../../util/noop');
const { isString } = require('../../../util/is');
const {
wrapString,
columnize: columnize_,
} = require('../../../formatter/wrappingFormatter');
const emptyStr = constant('');
class QueryCompiler_SQLite3 extends QueryCompiler {
constructor(client, builder, formatter) {
super(client, builder, formatter);
// The locks are not applicable in SQLite3
this.forShare = emptyStr;
this.forKeyShare = emptyStr;
this.forUpdate = emptyStr;
this.forNoKeyUpdate = emptyStr;
}
// SQLite requires us to build the multi-row insert as a listing of select with
// unions joining them together. So we'll build out this list of columns and
// then join them all together with select unions to complete the queries.
insert() {
const insertValues = this.single.insert || [];
let sql = this.with() + `insert into ${this.tableName} `;
if (Array.isArray(insertValues)) {
if (insertValues.length === 0) {
return '';
} else if (
insertValues.length === 1 &&
insertValues[0] &&
isEmpty(insertValues[0])
) {
return {
sql: sql + this._emptyInsertValue,
};
}
} else if (typeof insertValues === 'object' && isEmpty(insertValues)) {
return {
sql: sql + this._emptyInsertValue,
};
}
const insertData = this._prepInsert(insertValues);
if (isString(insertData)) {
return {
sql: sql + insertData,
};
}
if (insertData.columns.length === 0) {
return {
sql: '',
};
}
sql += `(${this.formatter.columnize(insertData.columns)})`;
// backwards compatible error
if (this.client.valueForUndefined !== null) {
insertData.values.forEach((bindings) => {
each(bindings, (binding) => {
if (binding === undefined)
throw new TypeError(
'`sqlite` does not support inserting default values. Specify ' +
'values explicitly or use the `useNullAsDefault` config flag. ' +
'(see docs https://knexjs.org/guide/query-builder.html#insert).'
);
});
});
}
if (insertData.values.length === 1) {
const parameters = this.client.parameterize(
insertData.values[0],
this.client.valueForUndefined,
this.builder,
this.bindingsHolder
);
sql += ` values (${parameters})`;
const { onConflict, ignore, merge } = this.single;
if (onConflict && ignore) sql += this._ignore(onConflict);
else if (onConflict && merge) {
sql += this._merge(merge.updates, onConflict, insertValues);
const wheres = this.where();
if (wheres) sql += ` ${wheres}`;
}
const { returning } = this.single;
if (returning) {
sql += this._returning(returning);
}
return {
sql,
returning,
};
}
const blocks = [];
let i = -1;
while (++i < insertData.values.length) {
let i2 = -1;
const block = (blocks[i] = []);
let current = insertData.values[i];
current = current === undefined ? this.client.valueForUndefined : current;
while (++i2 < insertData.columns.length) {
block.push(
this.client.alias(
this.client.parameter(
current[i2],
this.builder,
this.bindingsHolder
),
this.formatter.wrap(insertData.columns[i2])
)
);
}
blocks[i] = block.join(', ');
}
sql += ' select ' + blocks.join(' union all select ');
const { onConflict, ignore, merge } = this.single;
if (onConflict && ignore) sql += ' where true' + this._ignore(onConflict);
else if (onConflict && merge) {
sql +=
' where true' + this._merge(merge.updates, onConflict, insertValues);
}
const { returning } = this.single;
if (returning) sql += this._returning(returning);
return {
sql,
returning,
};
}
// Compiles an `update` query, allowing for a return value.
update() {
const withSQL = this.with();
const updateData = this._prepUpdate(this.single.update);
const wheres = this.where();
const { returning } = this.single;
return {
sql:
withSQL +
`update ${this.single.only ? 'only ' : ''}${this.tableName} ` +
`set ${updateData.join(', ')}` +
(wheres ? ` ${wheres}` : '') +
this._returning(returning),
returning,
};
}
_ignore(columns) {
if (columns === true) {
return ' on conflict do nothing';
}
return ` on conflict ${this._onConflictClause(columns)} do nothing`;
}
_merge(updates, columns, insert) {
let sql = ` on conflict ${this._onConflictClause(columns)} do update set `;
if (updates && Array.isArray(updates)) {
sql += updates
.map((column) =>
wrapString(
column.split('.').pop(),
this.formatter.builder,
this.client,
this.formatter
)
)
.map((column) => `${column} = excluded.${column}`)
.join(', ');
return sql;
} else if (updates && typeof updates === 'object') {
const updateData = this._prepUpdate(updates);
if (typeof updateData === 'string') {
sql += updateData;
} else {
sql += updateData.join(',');
}
return sql;
} else {
const insertData = this._prepInsert(insert);
if (typeof insertData === 'string') {
throw new Error(
'If using merge with a raw insert query, then updates must be provided'
);
}
sql += insertData.columns
.map((column) =>
wrapString(column.split('.').pop(), this.builder, this.client)
)
.map((column) => `${column} = excluded.${column}`)
.join(', ');
return sql;
}
}
_returning(value) {
return value ? ` returning ${this.formatter.columnize(value)}` : '';
}
// Compile a truncate table statement into SQL.
truncate() {
const { table } = this.single;
return {
sql: `delete from ${this.tableName}`,
output() {
return this.query({
sql: `delete from sqlite_sequence where name = '${table}'`,
}).catch(noop);
},
};
}
// Compiles a `columnInfo` query
columnInfo() {
const column = this.single.columnInfo;
// The user may have specified a custom wrapIdentifier function in the config. We
// need to run the identifiers through that function, but not format them as
// identifiers otherwise.
const table = this.client.customWrapIdentifier(this.single.table, identity);
return {
sql: `PRAGMA table_info(\`${table}\`)`,
output(resp) {
const maxLengthRegex = /.*\((\d+)\)/;
const out = reduce(
resp,
function (columns, val) {
let { type } = val;
let maxLength = type.match(maxLengthRegex);
if (maxLength) {
maxLength = maxLength[1];
}
type = maxLength ? type.split('(')[0] : type;
columns[val.name] = {
type: type.toLowerCase(),
maxLength,
nullable: !val.notnull,
defaultValue: val.dflt_value,
};
return columns;
},
{}
);
return (column && out[column]) || out;
},
};
}
limit() {
const noLimit = !this.single.limit && this.single.limit !== 0;
if (noLimit && !this.single.offset) return '';
// Workaround for offset only,
// see http://stackoverflow.com/questions/10491492/sqllite-with-skip-offset-only-not-limit
this.single.limit = noLimit ? -1 : this.single.limit;
return `limit ${this._getValueOrParameterFromAttribute('limit')}`;
}
// Json functions
jsonExtract(params) {
return this._jsonExtract('json_extract', params);
}
jsonSet(params) {
return this._jsonSet('json_set', params);
}
jsonInsert(params) {
return this._jsonSet('json_insert', params);
}
jsonRemove(params) {
const jsonCol = `json_remove(${columnize_(
params.column,
this.builder,
this.client,
this.bindingsHolder
)},${this.client.parameter(
params.path,
this.builder,
this.bindingsHolder
)})`;
return params.alias
? this.client.alias(jsonCol, this.formatter.wrap(params.alias))
: jsonCol;
}
whereJsonPath(statement) {
return this._whereJsonPath('json_extract', statement);
}
whereJsonSupersetOf(statement) {
throw new Error(
'Json superset where clause not actually supported by SQLite'
);
}
whereJsonSubsetOf(statement) {
throw new Error(
'Json subset where clause not actually supported by SQLite'
);
}
onJsonPathEquals(clause) {
return this._onJsonPathEquals('json_extract', clause);
}
}
module.exports = QueryCompiler_SQLite3;

View File

@ -0,0 +1,400 @@
// SQLite3_DDL
//
// All of the SQLite3 specific DDL helpers for renaming/dropping
// columns and changing datatypes.
// -------
const identity = require('lodash/identity');
const { nanonum } = require('../../../util/nanoid');
const {
copyData,
dropOriginal,
renameTable,
getTableSql,
isForeignCheckEnabled,
setForeignCheck,
executeForeignCheck,
} = require('./internal/sqlite-ddl-operations');
const { parseCreateTable, parseCreateIndex } = require('./internal/parser');
const {
compileCreateTable,
compileCreateIndex,
} = require('./internal/compiler');
const { isEqualId, includesId } = require('./internal/utils');
// So altering the schema in SQLite3 is a major pain.
// We have our own object to deal with the renaming and altering the types
// for sqlite3 things.
class SQLite3_DDL {
constructor(client, tableCompiler, pragma, connection) {
this.client = client;
this.tableCompiler = tableCompiler;
this.pragma = pragma;
this.tableNameRaw = this.tableCompiler.tableNameRaw;
this.alteredName = `_knex_temp_alter${nanonum(3)}`;
this.connection = connection;
this.formatter = (value) =>
this.client.customWrapIdentifier(value, identity);
this.wrap = (value) => this.client.wrapIdentifierImpl(value);
}
tableName() {
return this.formatter(this.tableNameRaw);
}
getTableSql() {
const tableName = this.tableName();
return this.client.transaction(
async (trx) => {
trx.disableProcessing();
const result = await trx.raw(getTableSql(tableName));
trx.enableProcessing();
return {
createTable: result.filter((create) => create.type === 'table')[0]
.sql,
createIndices: result
.filter((create) => create.type === 'index')
.map((create) => create.sql),
};
},
{ connection: this.connection }
);
}
async isForeignCheckEnabled() {
const result = await this.client
.raw(isForeignCheckEnabled())
.connection(this.connection);
return result[0].foreign_keys === 1;
}
async setForeignCheck(enable) {
await this.client.raw(setForeignCheck(enable)).connection(this.connection);
}
renameTable(trx) {
return trx.raw(renameTable(this.alteredName, this.tableName()));
}
dropOriginal(trx) {
return trx.raw(dropOriginal(this.tableName()));
}
copyData(trx, columns) {
return trx.raw(copyData(this.tableName(), this.alteredName, columns));
}
async alterColumn(columns) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.columns = parsedTable.columns.map((column) => {
const newColumnInfo = columns.find((c) => isEqualId(c.name, column.name));
if (newColumnInfo) {
column.type = newColumnInfo.type;
column.constraints.default =
newColumnInfo.defaultTo !== null
? {
name: null,
value: newColumnInfo.defaultTo,
expression: false,
}
: null;
column.constraints.notnull = newColumnInfo.notNull
? { name: null, conflict: null }
: null;
column.constraints.null = newColumnInfo.notNull
? null
: column.constraints.null;
}
return column;
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.generateAlterCommands(newTable, createIndices);
}
async dropColumn(columns) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.columns = parsedTable.columns.filter(
(parsedColumn) =>
parsedColumn.expression || !includesId(columns, parsedColumn.name)
);
if (parsedTable.columns.length === 0) {
throw new Error('Unable to drop last column from table');
}
parsedTable.constraints = parsedTable.constraints.filter((constraint) => {
if (constraint.type === 'PRIMARY KEY' || constraint.type === 'UNIQUE') {
return constraint.columns.every(
(constraintColumn) =>
constraintColumn.expression ||
!includesId(columns, constraintColumn.name)
);
} else if (constraint.type === 'FOREIGN KEY') {
return (
constraint.columns.every(
(constraintColumnName) => !includesId(columns, constraintColumnName)
) &&
(constraint.references.table !== parsedTable.table ||
constraint.references.columns.every(
(referenceColumnName) => !includesId(columns, referenceColumnName)
))
);
} else {
return true;
}
});
const newColumns = parsedTable.columns.map((column) => column.name);
const newTable = compileCreateTable(parsedTable, this.wrap);
const newIndices = [];
for (const createIndex of createIndices) {
const parsedIndex = parseCreateIndex(createIndex);
parsedIndex.columns = parsedIndex.columns.filter(
(parsedColumn) =>
parsedColumn.expression || !includesId(columns, parsedColumn.name)
);
if (parsedIndex.columns.length > 0) {
newIndices.push(compileCreateIndex(parsedIndex, this.wrap));
}
}
return this.alter(newTable, newIndices, newColumns);
}
async dropForeign(columns, foreignKeyName) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
if (!foreignKeyName) {
parsedTable.columns = parsedTable.columns.map((column) => ({
...column,
references: includesId(columns, column.name) ? null : column.references,
}));
}
parsedTable.constraints = parsedTable.constraints.filter((constraint) => {
if (constraint.type === 'FOREIGN KEY') {
if (foreignKeyName) {
return (
!constraint.name || !isEqualId(constraint.name, foreignKeyName)
);
}
return constraint.columns.every(
(constraintColumnName) => !includesId(columns, constraintColumnName)
);
} else {
return true;
}
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.alter(newTable, createIndices);
}
async dropPrimary(constraintName) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.columns = parsedTable.columns.map((column) => ({
...column,
primary: null,
}));
parsedTable.constraints = parsedTable.constraints.filter((constraint) => {
if (constraint.type === 'PRIMARY KEY') {
if (constraintName) {
return (
!constraint.name || !isEqualId(constraint.name, constraintName)
);
} else {
return false;
}
} else {
return true;
}
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.alter(newTable, createIndices);
}
async primary(columns, constraintName) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.columns = parsedTable.columns.map((column) => ({
...column,
primary: null,
}));
parsedTable.constraints = parsedTable.constraints.filter(
(constraint) => constraint.type !== 'PRIMARY KEY'
);
parsedTable.constraints.push({
type: 'PRIMARY KEY',
name: constraintName || null,
columns: columns.map((column) => ({
name: column,
expression: false,
collation: null,
order: null,
})),
conflict: null,
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.alter(newTable, createIndices);
}
async foreign(foreignInfo) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
parsedTable.constraints.push({
type: 'FOREIGN KEY',
name: foreignInfo.keyName || null,
columns: foreignInfo.column,
references: {
table: foreignInfo.inTable,
columns: foreignInfo.references,
delete: foreignInfo.onDelete || null,
update: foreignInfo.onUpdate || null,
match: null,
deferrable: null,
},
});
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.generateAlterCommands(newTable, createIndices);
}
async setNullable(column, isNullable) {
const { createTable, createIndices } = await this.getTableSql();
const parsedTable = parseCreateTable(createTable);
parsedTable.table = this.alteredName;
const parsedColumn = parsedTable.columns.find((c) =>
isEqualId(column, c.name)
);
if (!parsedColumn) {
throw new Error(
`.setNullable: Column ${column} does not exist in table ${this.tableName()}.`
);
}
parsedColumn.constraints.notnull = isNullable
? null
: { name: null, conflict: null };
parsedColumn.constraints.null = isNullable
? parsedColumn.constraints.null
: null;
const newTable = compileCreateTable(parsedTable, this.wrap);
return this.generateAlterCommands(newTable, createIndices);
}
async alter(newSql, createIndices, columns) {
const wasForeignCheckEnabled = await this.isForeignCheckEnabled();
if (wasForeignCheckEnabled) {
await this.setForeignCheck(false);
}
try {
await this.client.transaction(
async (trx) => {
await trx.raw(newSql);
await this.copyData(trx, columns);
await this.dropOriginal(trx);
await this.renameTable(trx);
for (const createIndex of createIndices) {
await trx.raw(createIndex);
}
if (wasForeignCheckEnabled) {
const foreignViolations = await trx.raw(executeForeignCheck());
if (foreignViolations.length > 0) {
throw new Error('FOREIGN KEY constraint failed');
}
}
},
{ connection: this.connection }
);
} finally {
if (wasForeignCheckEnabled) {
await this.setForeignCheck(true);
}
}
}
async generateAlterCommands(newSql, createIndices, columns) {
const sql = [];
const pre = [];
const post = [];
let check = null;
sql.push(newSql);
sql.push(copyData(this.tableName(), this.alteredName, columns));
sql.push(dropOriginal(this.tableName()));
sql.push(renameTable(this.alteredName, this.tableName()));
for (const createIndex of createIndices) {
sql.push(createIndex);
}
const isForeignCheckEnabled = await this.isForeignCheckEnabled();
if (isForeignCheckEnabled) {
pre.push(setForeignCheck(false));
post.push(setForeignCheck(true));
check = executeForeignCheck();
}
return { pre, sql, check, post };
}
}
module.exports = SQLite3_DDL;

View File

@ -0,0 +1,327 @@
function compileCreateTable(ast, wrap = (v) => v) {
return createTable(ast, wrap);
}
function compileCreateIndex(ast, wrap = (v) => v) {
return createIndex(ast, wrap);
}
function createTable(ast, wrap) {
return `CREATE${temporary(ast, wrap)} TABLE${exists(ast, wrap)} ${schema(
ast,
wrap
)}${table(ast, wrap)} (${columnDefinitionList(
ast,
wrap
)}${tableConstraintList(ast, wrap)})${rowid(ast, wrap)}`;
}
function temporary(ast, wrap) {
return ast.temporary ? ' TEMP' : '';
}
function rowid(ast, wrap) {
return ast.rowid ? ' WITHOUT ROWID' : '';
}
function columnDefinitionList(ast, wrap) {
return ast.columns.map((column) => columnDefinition(column, wrap)).join(', ');
}
function columnDefinition(ast, wrap) {
return `${identifier(ast.name, wrap)}${typeName(
ast,
wrap
)}${columnConstraintList(ast.constraints, wrap)}`;
}
function typeName(ast, wrap) {
return ast.type !== null ? ` ${ast.type}` : '';
}
function columnConstraintList(ast, wrap) {
return `${primaryColumnConstraint(ast, wrap)}${notnullColumnConstraint(
ast,
wrap
)}${nullColumnConstraint(ast, wrap)}${uniqueColumnConstraint(
ast,
wrap
)}${checkColumnConstraint(ast, wrap)}${defaultColumnConstraint(
ast,
wrap
)}${collateColumnConstraint(ast, wrap)}${referencesColumnConstraint(
ast,
wrap
)}${asColumnConstraint(ast, wrap)}`;
}
function primaryColumnConstraint(ast, wrap) {
return ast.primary !== null
? ` ${constraintName(ast.primary, wrap)}PRIMARY KEY${order(
ast.primary,
wrap
)}${conflictClause(ast.primary, wrap)}${autoincrement(ast.primary, wrap)}`
: '';
}
function autoincrement(ast, wrap) {
return ast.autoincrement ? ' AUTOINCREMENT' : '';
}
function notnullColumnConstraint(ast, wrap) {
return ast.notnull !== null
? ` ${constraintName(ast.notnull, wrap)}NOT NULL${conflictClause(
ast.notnull,
wrap
)}`
: '';
}
function nullColumnConstraint(ast, wrap) {
return ast.null !== null
? ` ${constraintName(ast.null, wrap)}NULL${conflictClause(ast.null, wrap)}`
: '';
}
function uniqueColumnConstraint(ast, wrap) {
return ast.unique !== null
? ` ${constraintName(ast.unique, wrap)}UNIQUE${conflictClause(
ast.unique,
wrap
)}`
: '';
}
function checkColumnConstraint(ast, wrap) {
return ast.check !== null
? ` ${constraintName(ast.check, wrap)}CHECK (${expression(
ast.check.expression,
wrap
)})`
: '';
}
function defaultColumnConstraint(ast, wrap) {
return ast.default !== null
? ` ${constraintName(ast.default, wrap)}DEFAULT ${
!ast.default.expression
? ast.default.value
: `(${expression(ast.default.value, wrap)})`
}`
: '';
}
function collateColumnConstraint(ast, wrap) {
return ast.collate !== null
? ` ${constraintName(ast.collate, wrap)}COLLATE ${ast.collate.collation}`
: '';
}
function referencesColumnConstraint(ast, wrap) {
return ast.references !== null
? ` ${constraintName(ast.references, wrap)}${foreignKeyClause(
ast.references,
wrap
)}`
: '';
}
function asColumnConstraint(ast, wrap) {
return ast.as !== null
? ` ${constraintName(ast.as, wrap)}${
ast.as.generated ? 'GENERATED ALWAYS ' : ''
}AS (${expression(ast.as.expression, wrap)})${
ast.as.mode !== null ? ` ${ast.as.mode}` : ''
}`
: '';
}
function tableConstraintList(ast, wrap) {
return ast.constraints.reduce(
(constraintList, constraint) =>
`${constraintList}, ${tableConstraint(constraint, wrap)}`,
''
);
}
function tableConstraint(ast, wrap) {
switch (ast.type) {
case 'PRIMARY KEY':
return primaryTableConstraint(ast, wrap);
case 'UNIQUE':
return uniqueTableConstraint(ast, wrap);
case 'CHECK':
return checkTableConstraint(ast, wrap);
case 'FOREIGN KEY':
return foreignTableConstraint(ast, wrap);
}
}
function primaryTableConstraint(ast, wrap) {
return `${constraintName(ast, wrap)}PRIMARY KEY (${indexedColumnList(
ast,
wrap
)})${conflictClause(ast, wrap)}`;
}
function uniqueTableConstraint(ast, wrap) {
return `${constraintName(ast, wrap)}UNIQUE (${indexedColumnList(
ast,
wrap
)})${conflictClause(ast, wrap)}`;
}
function conflictClause(ast, wrap) {
return ast.conflict !== null ? ` ON CONFLICT ${ast.conflict}` : '';
}
function checkTableConstraint(ast, wrap) {
return `${constraintName(ast, wrap)}CHECK (${expression(
ast.expression,
wrap
)})`;
}
function foreignTableConstraint(ast, wrap) {
return `${constraintName(ast, wrap)}FOREIGN KEY (${columnNameList(
ast,
wrap
)}) ${foreignKeyClause(ast.references, wrap)}`;
}
function foreignKeyClause(ast, wrap) {
return `REFERENCES ${table(ast, wrap)}${columnNameListOptional(
ast,
wrap
)}${deleteUpdateMatchList(ast, wrap)}${deferrable(ast.deferrable, wrap)}`;
}
function columnNameListOptional(ast, wrap) {
return ast.columns.length > 0 ? ` (${columnNameList(ast, wrap)})` : '';
}
function columnNameList(ast, wrap) {
return ast.columns.map((column) => identifier(column, wrap)).join(', ');
}
function deleteUpdateMatchList(ast, wrap) {
return `${deleteReference(ast, wrap)}${updateReference(
ast,
wrap
)}${matchReference(ast, wrap)}`;
}
function deleteReference(ast, wrap) {
return ast.delete !== null ? ` ON DELETE ${ast.delete}` : '';
}
function updateReference(ast, wrap) {
return ast.update !== null ? ` ON UPDATE ${ast.update}` : '';
}
function matchReference(ast, wrap) {
return ast.match !== null ? ` MATCH ${ast.match}` : '';
}
function deferrable(ast, wrap) {
return ast !== null
? ` ${ast.not ? 'NOT ' : ''}DEFERRABLE${
ast.initially !== null ? ` INITIALLY ${ast.initially}` : ''
}`
: '';
}
function constraintName(ast, wrap) {
return ast.name !== null ? `CONSTRAINT ${identifier(ast.name, wrap)} ` : '';
}
function createIndex(ast, wrap) {
return `CREATE${unique(ast, wrap)} INDEX${exists(ast, wrap)} ${schema(
ast,
wrap
)}${index(ast, wrap)} on ${table(ast, wrap)} (${indexedColumnList(
ast,
wrap
)})${where(ast, wrap)}`;
}
function unique(ast, wrap) {
return ast.unique ? ' UNIQUE' : '';
}
function exists(ast, wrap) {
return ast.exists ? ' IF NOT EXISTS' : '';
}
function schema(ast, wrap) {
return ast.schema !== null ? `${identifier(ast.schema, wrap)}.` : '';
}
function index(ast, wrap) {
return identifier(ast.index, wrap);
}
function table(ast, wrap) {
return identifier(ast.table, wrap);
}
function where(ast, wrap) {
return ast.where !== null ? ` where ${expression(ast.where)}` : '';
}
function indexedColumnList(ast, wrap) {
return ast.columns
.map((column) =>
!column.expression
? indexedColumn(column, wrap)
: indexedColumnExpression(column, wrap)
)
.join(', ');
}
function indexedColumn(ast, wrap) {
return `${identifier(ast.name, wrap)}${collation(ast, wrap)}${order(
ast,
wrap
)}`;
}
function indexedColumnExpression(ast, wrap) {
return `${indexedExpression(ast.name, wrap)}${collation(ast, wrap)}${order(
ast,
wrap
)}`;
}
function collation(ast, wrap) {
return ast.collation !== null ? ` COLLATE ${ast.collation}` : '';
}
function order(ast, wrap) {
return ast.order !== null ? ` ${ast.order}` : '';
}
function indexedExpression(ast, wrap) {
return expression(ast, wrap);
}
function expression(ast, wrap) {
return ast.reduce(
(expr, e) =>
Array.isArray(e)
? `${expr}(${expression(e)})`
: !expr
? e
: `${expr} ${e}`,
''
);
}
function identifier(ast, wrap) {
return wrap(ast);
}
module.exports = {
compileCreateTable,
compileCreateIndex,
};

View File

@ -0,0 +1,161 @@
// Sequence parser combinator
function s(sequence, post = (v) => v) {
return function ({ index = 0, input }) {
let position = index;
const ast = [];
for (const parser of sequence) {
const result = parser({ index: position, input });
if (result.success) {
position = result.index;
ast.push(result.ast);
} else {
return result;
}
}
return { success: true, ast: post(ast), index: position, input };
};
}
// Alternative parser combinator
function a(alternative, post = (v) => v) {
return function ({ index = 0, input }) {
for (const parser of alternative) {
const result = parser({ index, input });
if (result.success) {
return {
success: true,
ast: post(result.ast),
index: result.index,
input,
};
}
}
return { success: false, ast: null, index, input };
};
}
// Many parser combinator
function m(many, post = (v) => v) {
return function ({ index = 0, input }) {
let result = {};
let position = index;
const ast = [];
do {
result = many({ index: position, input });
if (result.success) {
position = result.index;
ast.push(result.ast);
}
} while (result.success);
if (ast.length > 0) {
return { success: true, ast: post(ast), index: position, input };
} else {
return { success: false, ast: null, index: position, input };
}
};
}
// Optional parser combinator
function o(optional, post = (v) => v) {
return function ({ index = 0, input }) {
const result = optional({ index, input });
if (result.success) {
return {
success: true,
ast: post(result.ast),
index: result.index,
input,
};
} else {
return { success: true, ast: post(null), index, input };
}
};
}
// Lookahead parser combinator
function l(lookahead, post = (v) => v) {
return function ({ index = 0, input }) {
const result = lookahead.do({ index, input });
if (result.success) {
const resultNext = lookahead.next({ index: result.index, input });
if (resultNext.success) {
return {
success: true,
ast: post(result.ast),
index: result.index,
input,
};
}
}
return { success: false, ast: null, index, input };
};
}
// Negative parser combinator
function n(negative, post = (v) => v) {
return function ({ index = 0, input }) {
const result = negative.do({ index, input });
if (result.success) {
const resultNot = negative.not({ index, input });
if (!resultNot.success) {
return {
success: true,
ast: post(result.ast),
index: result.index,
input,
};
}
}
return { success: false, ast: null, index, input };
};
}
// Token parser combinator
function t(token, post = (v) => v.text) {
return function ({ index = 0, input }) {
const result = input[index];
if (
result !== undefined &&
(token.type === undefined || token.type === result.type) &&
(token.text === undefined ||
token.text.toUpperCase() === result.text.toUpperCase())
) {
return {
success: true,
ast: post(result),
index: index + 1,
input,
};
} else {
return { success: false, ast: null, index, input };
}
};
}
// Empty parser constant
const e = function ({ index = 0, input }) {
return { success: true, ast: null, index, input };
};
// Finish parser constant
const f = function ({ index = 0, input }) {
return { success: index === input.length, ast: null, index, input };
};
module.exports = { s, a, m, o, l, n, t, e, f };

View File

@ -0,0 +1,638 @@
const { tokenize } = require('./tokenizer');
const { s, a, m, o, l, n, t, e, f } = require('./parser-combinator');
const TOKENS = {
keyword:
/(?:ABORT|ACTION|ADD|AFTER|ALL|ALTER|ALWAYS|ANALYZE|AND|AS|ASC|ATTACH|AUTOINCREMENT|BEFORE|BEGIN|BETWEEN|BY|CASCADE|CASE|CAST|CHECK|COLLATE|COLUMN|COMMIT|CONFLICT|CONSTRAINT|CREATE|CROSS|CURRENT|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP|DATABASE|DEFAULT|DEFERRED|DEFERRABLE|DELETE|DESC|DETACH|DISTINCT|DO|DROP|END|EACH|ELSE|ESCAPE|EXCEPT|EXCLUSIVE|EXCLUDE|EXISTS|EXPLAIN|FAIL|FILTER|FIRST|FOLLOWING|FOR|FOREIGN|FROM|FULL|GENERATED|GLOB|GROUP|GROUPS|HAVING|IF|IGNORE|IMMEDIATE|IN|INDEX|INDEXED|INITIALLY|INNER|INSERT|INSTEAD|INTERSECT|INTO|IS|ISNULL|JOIN|KEY|LAST|LEFT|LIKE|LIMIT|MATCH|MATERIALIZED|NATURAL|NO|NOT|NOTHING|NOTNULL|NULL|NULLS|OF|OFFSET|ON|OR|ORDER|OTHERS|OUTER|OVER|PARTITION|PLAN|PRAGMA|PRECEDING|PRIMARY|QUERY|RAISE|RANGE|RECURSIVE|REFERENCES|REGEXP|REINDEX|RELEASE|RENAME|REPLACE|RESTRICT|RETURNING|RIGHT|ROLLBACK|ROW|ROWS|SAVEPOINT|SELECT|SET|TABLE|TEMP|TEMPORARY|THEN|TIES|TO|TRANSACTION|TRIGGER|UNBOUNDED|UNION|UNIQUE|UPDATE|USING|VACUUM|VALUES|VIEW|VIRTUAL|WHEN|WHERE|WINDOW|WITH|WITHOUT)(?=\s+|-|\(|\)|;|\+|\*|\/|%|==|=|<=|<>|<<|<|>=|>>|>|!=|,|&|~|\|\||\||\.)/,
id: /"[^"]*(?:""[^"]*)*"|`[^`]*(?:``[^`]*)*`|\[[^[\]]*\]|[a-z_][a-z0-9_$]*/,
string: /'[^']*(?:''[^']*)*'/,
blob: /x'(?:[0-9a-f][0-9a-f])+'/,
numeric: /(?:\d+(?:\.\d*)?|\.\d+)(?:e(?:\+|-)?\d+)?|0x[0-9a-f]+/,
variable: /\?\d*|[@$:][a-z0-9_$]+/,
operator: /-|\(|\)|;|\+|\*|\/|%|==|=|<=|<>|<<|<|>=|>>|>|!=|,|&|~|\|\||\||\./,
_ws: /\s+/,
};
function parseCreateTable(sql) {
const result = createTable({ input: tokenize(sql, TOKENS) });
if (!result.success) {
throw new Error(
`Parsing CREATE TABLE failed at [${result.input
.slice(result.index)
.map((t) => t.text)
.join(' ')}] of "${sql}"`
);
}
return result.ast;
}
function parseCreateIndex(sql) {
const result = createIndex({ input: tokenize(sql, TOKENS) });
if (!result.success) {
throw new Error(
`Parsing CREATE INDEX failed at [${result.input
.slice(result.index)
.map((t) => t.text)
.join(' ')}] of "${sql}"`
);
}
return result.ast;
}
function createTable(ctx) {
return s(
[
t({ text: 'CREATE' }, (v) => null),
temporary,
t({ text: 'TABLE' }, (v) => null),
exists,
schema,
table,
t({ text: '(' }, (v) => null),
columnDefinitionList,
tableConstraintList,
t({ text: ')' }, (v) => null),
rowid,
f,
],
(v) => Object.assign({}, ...v.filter((x) => x !== null))
)(ctx);
}
function temporary(ctx) {
return a([t({ text: 'TEMP' }), t({ text: 'TEMPORARY' }), e], (v) => ({
temporary: v !== null,
}))(ctx);
}
function rowid(ctx) {
return o(s([t({ text: 'WITHOUT' }), t({ text: 'ROWID' })]), (v) => ({
rowid: v !== null,
}))(ctx);
}
function columnDefinitionList(ctx) {
return a([
s([columnDefinition, t({ text: ',' }), columnDefinitionList], (v) => ({
columns: [v[0]].concat(v[2].columns),
})),
s([columnDefinition], (v) => ({ columns: [v[0]] })),
])(ctx);
}
function columnDefinition(ctx) {
return s(
[s([identifier], (v) => ({ name: v[0] })), typeName, columnConstraintList],
(v) => Object.assign({}, ...v)
)(ctx);
}
function typeName(ctx) {
return o(
s(
[
m(t({ type: 'id' })),
a([
s(
[
t({ text: '(' }),
signedNumber,
t({ text: ',' }),
signedNumber,
t({ text: ')' }),
],
(v) => `(${v[1]}, ${v[3]})`
),
s(
[t({ text: '(' }), signedNumber, t({ text: ')' })],
(v) => `(${v[1]})`
),
e,
]),
],
(v) => `${v[0].join(' ')}${v[1] || ''}`
),
(v) => ({ type: v })
)(ctx);
}
function columnConstraintList(ctx) {
return o(m(columnConstraint), (v) => ({
constraints: Object.assign(
{
primary: null,
notnull: null,
null: null,
unique: null,
check: null,
default: null,
collate: null,
references: null,
as: null,
},
...(v || [])
),
}))(ctx);
}
function columnConstraint(ctx) {
return a([
primaryColumnConstraint,
notnullColumnConstraint,
nullColumnConstraint,
uniqueColumnConstraint,
checkColumnConstraint,
defaultColumnConstraint,
collateColumnConstraint,
referencesColumnConstraint,
asColumnConstraint,
])(ctx);
}
function primaryColumnConstraint(ctx) {
return s(
[
constraintName,
t({ text: 'PRIMARY' }, (v) => null),
t({ text: 'KEY' }, (v) => null),
order,
conflictClause,
autoincrement,
],
(v) => ({ primary: Object.assign({}, ...v.filter((x) => x !== null)) })
)(ctx);
}
function autoincrement(ctx) {
return o(t({ text: 'AUTOINCREMENT' }), (v) => ({
autoincrement: v !== null,
}))(ctx);
}
function notnullColumnConstraint(ctx) {
return s(
[
constraintName,
t({ text: 'NOT' }, (v) => null),
t({ text: 'NULL' }, (v) => null),
conflictClause,
],
(v) => ({ notnull: Object.assign({}, ...v.filter((x) => x !== null)) })
)(ctx);
}
function nullColumnConstraint(ctx) {
return s(
[constraintName, t({ text: 'NULL' }, (v) => null), conflictClause],
(v) => ({ null: Object.assign({}, ...v.filter((x) => x !== null)) })
)(ctx);
}
function uniqueColumnConstraint(ctx) {
return s(
[constraintName, t({ text: 'UNIQUE' }, (v) => null), conflictClause],
(v) => ({ unique: Object.assign({}, ...v.filter((x) => x !== null)) })
)(ctx);
}
function checkColumnConstraint(ctx) {
return s(
[
constraintName,
t({ text: 'CHECK' }, (v) => null),
t({ text: '(' }, (v) => null),
s([expression], (v) => ({ expression: v[0] })),
t({ text: ')' }, (v) => null),
],
(v) => ({ check: Object.assign({}, ...v.filter((x) => x !== null)) })
)(ctx);
}
function defaultColumnConstraint(ctx) {
return s(
[
constraintName,
t({ text: 'DEFAULT' }, (v) => null),
a([
s([t({ text: '(' }), expression, t({ text: ')' })], (v) => ({
value: v[1],
expression: true,
})),
s([literalValue], (v) => ({ value: v[0], expression: false })),
s([signedNumber], (v) => ({ value: v[0], expression: false })),
]),
],
(v) => ({ default: Object.assign({}, ...v.filter((x) => x !== null)) })
)(ctx);
}
function collateColumnConstraint(ctx) {
return s(
[
constraintName,
t({ text: 'COLLATE' }, (v) => null),
t({ type: 'id' }, (v) => ({ collation: v.text })),
],
(v) => ({ collate: Object.assign({}, ...v.filter((x) => x !== null)) })
)(ctx);
}
function referencesColumnConstraint(ctx) {
return s(
[constraintName, s([foreignKeyClause], (v) => v[0].references)],
(v) => ({
references: Object.assign({}, ...v.filter((x) => x !== null)),
})
)(ctx);
}
function asColumnConstraint(ctx) {
return s(
[
constraintName,
o(s([t({ text: 'GENERATED' }), t({ text: 'ALWAYS' })]), (v) => ({
generated: v !== null,
})),
t({ text: 'AS' }, (v) => null),
t({ text: '(' }, (v) => null),
s([expression], (v) => ({ expression: v[0] })),
t({ text: ')' }, (v) => null),
a([t({ text: 'STORED' }), t({ text: 'VIRTUAL' }), e], (v) => ({
mode: v ? v.toUpperCase() : null,
})),
],
(v) => ({ as: Object.assign({}, ...v.filter((x) => x !== null)) })
)(ctx);
}
function tableConstraintList(ctx) {
return o(m(s([t({ text: ',' }), tableConstraint], (v) => v[1])), (v) => ({
constraints: v || [],
}))(ctx);
}
function tableConstraint(ctx) {
return a([
primaryTableConstraint,
uniqueTableConstraint,
checkTableConstraint,
foreignTableConstraint,
])(ctx);
}
function primaryTableConstraint(ctx) {
return s(
[
constraintName,
t({ text: 'PRIMARY' }, (v) => null),
t({ text: 'KEY' }, (v) => null),
t({ text: '(' }, (v) => null),
indexedColumnList,
t({ text: ')' }, (v) => null),
conflictClause,
],
(v) =>
Object.assign({ type: 'PRIMARY KEY' }, ...v.filter((x) => x !== null))
)(ctx);
}
function uniqueTableConstraint(ctx) {
return s(
[
constraintName,
t({ text: 'UNIQUE' }, (v) => null),
t({ text: '(' }, (v) => null),
indexedColumnList,
t({ text: ')' }, (v) => null),
conflictClause,
],
(v) => Object.assign({ type: 'UNIQUE' }, ...v.filter((x) => x !== null))
)(ctx);
}
function conflictClause(ctx) {
return o(
s(
[
t({ text: 'ON' }),
t({ text: 'CONFLICT' }),
a([
t({ text: 'ROLLBACK' }),
t({ text: 'ABORT' }),
t({ text: 'FAIL' }),
t({ text: 'IGNORE' }),
t({ text: 'REPLACE' }),
]),
],
(v) => v[2]
),
(v) => ({ conflict: v ? v.toUpperCase() : null })
)(ctx);
}
function checkTableConstraint(ctx) {
return s(
[
constraintName,
t({ text: 'CHECK' }, (v) => null),
t({ text: '(' }, (v) => null),
s([expression], (v) => ({ expression: v[0] })),
t({ text: ')' }, (v) => null),
],
(v) => Object.assign({ type: 'CHECK' }, ...v.filter((x) => x !== null))
)(ctx);
}
function foreignTableConstraint(ctx) {
return s(
[
constraintName,
t({ text: 'FOREIGN' }, (v) => null),
t({ text: 'KEY' }, (v) => null),
t({ text: '(' }, (v) => null),
columnNameList,
t({ text: ')' }, (v) => null),
foreignKeyClause,
],
(v) =>
Object.assign({ type: 'FOREIGN KEY' }, ...v.filter((x) => x !== null))
)(ctx);
}
function foreignKeyClause(ctx) {
return s(
[
t({ text: 'REFERENCES' }, (v) => null),
table,
columnNameListOptional,
o(m(a([deleteReference, updateReference, matchReference])), (v) =>
Object.assign({ delete: null, update: null, match: null }, ...(v || []))
),
deferrable,
],
(v) => ({ references: Object.assign({}, ...v.filter((x) => x !== null)) })
)(ctx);
}
function columnNameListOptional(ctx) {
return o(
s([t({ text: '(' }), columnNameList, t({ text: ')' })], (v) => v[1]),
(v) => ({ columns: v ? v.columns : [] })
)(ctx);
}
function columnNameList(ctx) {
return s(
[
o(m(s([identifier, t({ text: ',' })], (v) => v[0])), (v) =>
v !== null ? v : []
),
identifier,
],
(v) => ({ columns: v[0].concat([v[1]]) })
)(ctx);
}
function deleteReference(ctx) {
return s([t({ text: 'ON' }), t({ text: 'DELETE' }), onAction], (v) => ({
delete: v[2],
}))(ctx);
}
function updateReference(ctx) {
return s([t({ text: 'ON' }), t({ text: 'UPDATE' }), onAction], (v) => ({
update: v[2],
}))(ctx);
}
function matchReference(ctx) {
return s(
[t({ text: 'MATCH' }), a([t({ type: 'keyword' }), t({ type: 'id' })])],
(v) => ({ match: v[1] })
)(ctx);
}
function deferrable(ctx) {
return o(
s([
o(t({ text: 'NOT' })),
t({ text: 'DEFERRABLE' }),
o(
s(
[
t({ text: 'INITIALLY' }),
a([t({ text: 'DEFERRED' }), t({ text: 'IMMEDIATE' })]),
],
(v) => v[1].toUpperCase()
)
),
]),
(v) => ({ deferrable: v ? { not: v[0] !== null, initially: v[2] } : null })
)(ctx);
}
function constraintName(ctx) {
return o(
s([t({ text: 'CONSTRAINT' }), identifier], (v) => v[1]),
(v) => ({ name: v })
)(ctx);
}
function createIndex(ctx) {
return s(
[
t({ text: 'CREATE' }, (v) => null),
unique,
t({ text: 'INDEX' }, (v) => null),
exists,
schema,
index,
t({ text: 'ON' }, (v) => null),
table,
t({ text: '(' }, (v) => null),
indexedColumnList,
t({ text: ')' }, (v) => null),
where,
f,
],
(v) => Object.assign({}, ...v.filter((x) => x !== null))
)(ctx);
}
function unique(ctx) {
return o(t({ text: 'UNIQUE' }), (v) => ({ unique: v !== null }))(ctx);
}
function exists(ctx) {
return o(
s([t({ text: 'IF' }), t({ text: 'NOT' }), t({ text: 'EXISTS' })]),
(v) => ({ exists: v !== null })
)(ctx);
}
function schema(ctx) {
return o(
s([identifier, t({ text: '.' })], (v) => v[0]),
(v) => ({ schema: v })
)(ctx);
}
function index(ctx) {
return s([identifier], (v) => ({ index: v[0] }))(ctx);
}
function table(ctx) {
return s([identifier], (v) => ({ table: v[0] }))(ctx);
}
function where(ctx) {
return o(
s([t({ text: 'WHERE' }), expression], (v) => v[1]),
(v) => ({ where: v })
)(ctx);
}
function indexedColumnList(ctx) {
return a([
s([indexedColumn, t({ text: ',' }), indexedColumnList], (v) => ({
columns: [v[0]].concat(v[2].columns),
})),
s([indexedColumnExpression, t({ text: ',' }), indexedColumnList], (v) => ({
columns: [v[0]].concat(v[2].columns),
})),
l({ do: indexedColumn, next: t({ text: ')' }) }, (v) => ({
columns: [v],
})),
l({ do: indexedColumnExpression, next: t({ text: ')' }) }, (v) => ({
columns: [v],
})),
])(ctx);
}
function indexedColumn(ctx) {
return s(
[
s([identifier], (v) => ({ name: v[0], expression: false })),
collation,
order,
],
(v) => Object.assign({}, ...v.filter((x) => x !== null))
)(ctx);
}
function indexedColumnExpression(ctx) {
return s(
[
s([indexedExpression], (v) => ({ name: v[0], expression: true })),
collation,
order,
],
(v) => Object.assign({}, ...v.filter((x) => x !== null))
)(ctx);
}
function collation(ctx) {
return o(
s([t({ text: 'COLLATE' }), t({ type: 'id' })], (v) => v[1]),
(v) => ({ collation: v })
)(ctx);
}
function order(ctx) {
return a([t({ text: 'ASC' }), t({ text: 'DESC' }), e], (v) => ({
order: v ? v.toUpperCase() : null,
}))(ctx);
}
function indexedExpression(ctx) {
return m(
a([
n({
do: t({ type: 'keyword' }),
not: a([
t({ text: 'COLLATE' }),
t({ text: 'ASC' }),
t({ text: 'DESC' }),
]),
}),
t({ type: 'id' }),
t({ type: 'string' }),
t({ type: 'blob' }),
t({ type: 'numeric' }),
t({ type: 'variable' }),
n({
do: t({ type: 'operator' }),
not: a([t({ text: '(' }), t({ text: ')' }), t({ text: ',' })]),
}),
s([t({ text: '(' }), o(expression), t({ text: ')' })], (v) => v[1] || []),
])
)(ctx);
}
function expression(ctx) {
return m(
a([
t({ type: 'keyword' }),
t({ type: 'id' }),
t({ type: 'string' }),
t({ type: 'blob' }),
t({ type: 'numeric' }),
t({ type: 'variable' }),
n({
do: t({ type: 'operator' }),
not: a([t({ text: '(' }), t({ text: ')' })]),
}),
s([t({ text: '(' }), o(expression), t({ text: ')' })], (v) => v[1] || []),
])
)(ctx);
}
function identifier(ctx) {
return a([t({ type: 'id' }), t({ type: 'string' })], (v) =>
/^["`['][^]*["`\]']$/.test(v) ? v.substring(1, v.length - 1) : v
)(ctx);
}
function onAction(ctx) {
return a(
[
s([t({ text: 'SET' }), t({ text: 'NULL' })], (v) => `${v[0]} ${v[1]}`),
s([t({ text: 'SET' }), t({ text: 'DEFAULT' })], (v) => `${v[0]} ${v[1]}`),
t({ text: 'CASCADE' }),
t({ text: 'RESTRICT' }),
s([t({ text: 'NO' }), t({ text: 'ACTION' })], (v) => `${v[0]} ${v[1]}`),
],
(v) => v.toUpperCase()
)(ctx);
}
function literalValue(ctx) {
return a([
t({ type: 'numeric' }),
t({ type: 'string' }),
t({ type: 'id' }),
t({ type: 'blob' }),
t({ text: 'NULL' }),
t({ text: 'TRUE' }),
t({ text: 'FALSE' }),
t({ text: 'CURRENT_TIME' }),
t({ text: 'CURRENT_DATE' }),
t({ text: 'CURRENT_TIMESTAMP' }),
])(ctx);
}
function signedNumber(ctx) {
return s(
[a([t({ text: '+' }), t({ text: '-' }), e]), t({ type: 'numeric' })],
(v) => `${v[0] || ''}${v[1]}`
)(ctx);
}
module.exports = {
parseCreateTable,
parseCreateIndex,
};

View File

@ -0,0 +1,41 @@
function copyData(sourceTable, targetTable, columns) {
return `INSERT INTO "${targetTable}" SELECT ${
columns === undefined
? '*'
: columns.map((column) => `"${column}"`).join(', ')
} FROM "${sourceTable}";`;
}
function dropOriginal(tableName) {
return `DROP TABLE "${tableName}"`;
}
function renameTable(tableName, alteredName) {
return `ALTER TABLE "${tableName}" RENAME TO "${alteredName}"`;
}
function getTableSql(tableName) {
return `SELECT type, sql FROM sqlite_master WHERE (type='table' OR (type='index' AND sql IS NOT NULL)) AND lower(tbl_name)='${tableName.toLowerCase()}'`;
}
function isForeignCheckEnabled() {
return `PRAGMA foreign_keys`;
}
function setForeignCheck(enable) {
return `PRAGMA foreign_keys = ${enable ? 'ON' : 'OFF'}`;
}
function executeForeignCheck() {
return `PRAGMA foreign_key_check`;
}
module.exports = {
copyData,
dropOriginal,
renameTable,
getTableSql,
isForeignCheckEnabled,
setForeignCheck,
executeForeignCheck,
};

View File

@ -0,0 +1,38 @@
function tokenize(text, tokens) {
const compiledRegex = new RegExp(
Object.entries(tokens)
.map(([type, regex]) => `(?<${type}>${regex.source})`)
.join('|'),
'yi'
);
let index = 0;
const ast = [];
while (index < text.length) {
compiledRegex.lastIndex = index;
const result = text.match(compiledRegex);
if (result !== null) {
const [type, text] = Object.entries(result.groups).find(
([name, group]) => group !== undefined
);
index += text.length;
if (!type.startsWith('_')) {
ast.push({ type, text });
}
} else {
throw new Error(
`No matching tokenizer rule found at: [${text.substring(index)}]`
);
}
}
return ast;
}
module.exports = {
tokenize,
};

View File

@ -0,0 +1,12 @@
function isEqualId(first, second) {
return first.toLowerCase() === second.toLowerCase();
}
function includesId(list, id) {
return list.some((item) => isEqualId(item, id));
}
module.exports = {
isEqualId,
includesId,
};

View File

@ -0,0 +1,50 @@
const ColumnCompiler = require('../../../schema/columncompiler');
// Column Compiler
// -------
class ColumnCompiler_SQLite3 extends ColumnCompiler {
constructor() {
super(...arguments);
this.modifiers = ['nullable', 'defaultTo'];
this._addCheckModifiers();
}
// Types
// -------
enu(allowed) {
return `text check (${this.formatter.wrap(
this.args[0]
)} in ('${allowed.join("', '")}'))`;
}
_pushAlterCheckQuery(checkPredicate, constraintName) {
throw new Error(
`Alter table with to add constraints is not permitted in SQLite`
);
}
checkRegex(regexes, constraintName) {
return this._check(
`${this.formatter.wrap(
this.getColumnName()
)} REGEXP ${this.client._escapeBinding(regexes)}`,
constraintName
);
}
}
ColumnCompiler_SQLite3.prototype.json = 'json';
ColumnCompiler_SQLite3.prototype.jsonb = 'json';
ColumnCompiler_SQLite3.prototype.double =
ColumnCompiler_SQLite3.prototype.decimal =
ColumnCompiler_SQLite3.prototype.floating =
'float';
ColumnCompiler_SQLite3.prototype.timestamp = 'datetime';
// autoincrement without primary key is a syntax error in SQLite, so it's necessary
ColumnCompiler_SQLite3.prototype.increments =
ColumnCompiler_SQLite3.prototype.bigincrements =
'integer not null primary key autoincrement';
module.exports = ColumnCompiler_SQLite3;

View File

@ -0,0 +1,80 @@
// SQLite3: Column Builder & Compiler
// -------
const SchemaCompiler = require('../../../schema/compiler');
const some = require('lodash/some');
// Schema Compiler
// -------
class SchemaCompiler_SQLite3 extends SchemaCompiler {
constructor(client, builder) {
super(client, builder);
}
// Compile the query to determine if a table exists.
hasTable(tableName) {
const sql =
`select * from sqlite_master ` +
`where type = 'table' and name = ${this.client.parameter(
this.formatter.wrap(tableName).replace(/`/g, ''),
this.builder,
this.bindingsHolder
)}`;
this.pushQuery({ sql, output: (resp) => resp.length > 0 });
}
// Compile the query to determine if a column exists.
hasColumn(tableName, column) {
this.pushQuery({
sql: `PRAGMA table_info(${this.formatter.wrap(tableName)})`,
output(resp) {
return some(resp, (col) => {
return (
this.client.wrapIdentifier(col.name.toLowerCase()) ===
this.client.wrapIdentifier(column.toLowerCase())
);
});
},
});
}
// Compile a rename table command.
renameTable(from, to) {
this.pushQuery(
`alter table ${this.formatter.wrap(from)} rename to ${this.formatter.wrap(
to
)}`
);
}
async generateDdlCommands() {
const sequence = this.builder._sequence;
for (let i = 0, l = sequence.length; i < l; i++) {
const query = sequence[i];
this[query.method].apply(this, query.args);
}
const commandSources = this.sequence;
if (commandSources.length === 1 && commandSources[0].statementsProducer) {
return commandSources[0].statementsProducer();
} else {
const result = [];
for (const commandSource of commandSources) {
const command = commandSource.sql;
if (Array.isArray(command)) {
result.push(...command);
} else {
result.push(command);
}
}
return { pre: [], sql: result, check: null, post: [] };
}
}
}
module.exports = SchemaCompiler_SQLite3;

View File

@ -0,0 +1,347 @@
const filter = require('lodash/filter');
const values = require('lodash/values');
const identity = require('lodash/identity');
const { isObject } = require('../../../util/is');
const TableCompiler = require('../../../schema/tablecompiler');
const { formatDefault } = require('../../../formatter/formatterUtils');
class TableCompiler_SQLite3 extends TableCompiler {
constructor() {
super(...arguments);
}
// Create a new table.
createQuery(columns, ifNot, like) {
const createStatement = ifNot
? 'create table if not exists '
: 'create table ';
let sql = createStatement + this.tableName();
if (like && this.tableNameLike()) {
sql += ' as select * from ' + this.tableNameLike() + ' where 0=1';
} else {
// so we will need to check for a primary key commands and add the columns
// to the table's declaration here so they can be created on the tables.
sql += ' (' + columns.sql.join(', ');
sql += this.foreignKeys() || '';
sql += this.primaryKeys() || '';
sql += this._addChecks();
sql += ')';
}
this.pushQuery(sql);
if (like) {
this.addColumns(columns, this.addColumnsPrefix);
}
}
addColumns(columns, prefix, colCompilers) {
if (prefix === this.alterColumnsPrefix) {
const compiler = this;
const columnsInfo = colCompilers.map((col) => {
const name = this.client.customWrapIdentifier(
col.getColumnName(),
identity,
col.columnBuilder.queryContext()
);
const type = col.getColumnType();
const defaultTo = col.modified['defaultTo']
? formatDefault(col.modified['defaultTo'][0], col.type, this.client)
: null;
const notNull =
col.modified['nullable'] && col.modified['nullable'][0] === false;
return { name, type, defaultTo, notNull };
});
this.pushQuery({
sql: `PRAGMA table_info(${this.tableName()})`,
statementsProducer(pragma, connection) {
return compiler.client
.ddl(compiler, pragma, connection)
.alterColumn(columnsInfo);
},
});
} else {
for (let i = 0, l = columns.sql.length; i < l; i++) {
this.pushQuery({
sql: `alter table ${this.tableName()} add column ${columns.sql[i]}`,
bindings: columns.bindings[i],
});
}
}
}
// Compile a drop unique key command.
dropUnique(columns, indexName) {
indexName = indexName
? this.formatter.wrap(indexName)
: this._indexCommand('unique', this.tableNameRaw, columns);
this.pushQuery(`drop index ${indexName}`);
}
// Compile a drop foreign key command.
dropForeign(columns, indexName) {
const compiler = this;
columns = Array.isArray(columns) ? columns : [columns];
columns = columns.map((column) =>
this.client.customWrapIdentifier(column, identity)
);
indexName = this.client.customWrapIdentifier(indexName, identity);
this.pushQuery({
sql: `PRAGMA table_info(${this.tableName()})`,
output(pragma) {
return compiler.client
.ddl(compiler, pragma, this.connection)
.dropForeign(columns, indexName);
},
});
}
// Compile a drop primary key command.
dropPrimary(constraintName) {
const compiler = this;
constraintName = this.client.customWrapIdentifier(constraintName, identity);
this.pushQuery({
sql: `PRAGMA table_info(${this.tableName()})`,
output(pragma) {
return compiler.client
.ddl(compiler, pragma, this.connection)
.dropPrimary(constraintName);
},
});
}
dropIndex(columns, indexName) {
indexName = indexName
? this.formatter.wrap(indexName)
: this._indexCommand('index', this.tableNameRaw, columns);
this.pushQuery(`drop index ${indexName}`);
}
// Compile a unique key command.
unique(columns, indexName) {
let deferrable;
let predicate;
if (isObject(indexName)) {
({ indexName, deferrable, predicate } = indexName);
}
if (deferrable && deferrable !== 'not deferrable') {
this.client.logger.warn(
`sqlite3: unique index \`${indexName}\` will not be deferrable ${deferrable} because sqlite3 does not support deferred constraints.`
);
}
indexName = indexName
? this.formatter.wrap(indexName)
: this._indexCommand('unique', this.tableNameRaw, columns);
columns = this.formatter.columnize(columns);
const predicateQuery = predicate
? ' ' + this.client.queryCompiler(predicate).where()
: '';
this.pushQuery(
`create unique index ${indexName} on ${this.tableName()} (${columns})${predicateQuery}`
);
}
// Compile a plain index key command.
index(columns, indexName, options) {
indexName = indexName
? this.formatter.wrap(indexName)
: this._indexCommand('index', this.tableNameRaw, columns);
columns = this.formatter.columnize(columns);
let predicate;
if (isObject(options)) {
({ predicate } = options);
}
const predicateQuery = predicate
? ' ' + this.client.queryCompiler(predicate).where()
: '';
this.pushQuery(
`create index ${indexName} on ${this.tableName()} (${columns})${predicateQuery}`
);
}
/**
* Add a primary key to an existing table.
*
* @NOTE The `createQuery` method above handles table creation. Don't do anything regarding table
* creation in this method
*
* @param {string | string[]} columns - Column name(s) to assign as primary keys
* @param {string} [constraintName] - Custom name for the PK constraint
*/
primary(columns, constraintName) {
const compiler = this;
columns = Array.isArray(columns) ? columns : [columns];
columns = columns.map((column) =>
this.client.customWrapIdentifier(column, identity)
);
let deferrable;
if (isObject(constraintName)) {
({ constraintName, deferrable } = constraintName);
}
if (deferrable && deferrable !== 'not deferrable') {
this.client.logger.warn(
`sqlite3: primary key constraint \`${constraintName}\` will not be deferrable ${deferrable} because sqlite3 does not support deferred constraints.`
);
}
constraintName = this.client.customWrapIdentifier(constraintName, identity);
if (this.method !== 'create' && this.method !== 'createIfNot') {
this.pushQuery({
sql: `PRAGMA table_info(${this.tableName()})`,
output(pragma) {
return compiler.client
.ddl(compiler, pragma, this.connection)
.primary(columns, constraintName);
},
});
}
}
/**
* Add a foreign key constraint to an existing table
*
* @NOTE The `createQuery` method above handles foreign key constraints on table creation. Don't do
* anything regarding table creation in this method
*
* @param {object} foreignInfo - Information about the current column foreign setup
* @param {string | string[]} [foreignInfo.column] - Column in the current constraint
* @param {string | undefined} foreignInfo.keyName - Name of the foreign key constraint
* @param {string | string[]} foreignInfo.references - What column it references in the other table
* @param {string} foreignInfo.inTable - What table is referenced in this constraint
* @param {string} [foreignInfo.onUpdate] - What to do on updates
* @param {string} [foreignInfo.onDelete] - What to do on deletions
*/
foreign(foreignInfo) {
const compiler = this;
if (this.method !== 'create' && this.method !== 'createIfNot') {
foreignInfo.column = Array.isArray(foreignInfo.column)
? foreignInfo.column
: [foreignInfo.column];
foreignInfo.column = foreignInfo.column.map((column) =>
this.client.customWrapIdentifier(column, identity)
);
foreignInfo.inTable = this.client.customWrapIdentifier(
foreignInfo.inTable,
identity
);
foreignInfo.references = Array.isArray(foreignInfo.references)
? foreignInfo.references
: [foreignInfo.references];
foreignInfo.references = foreignInfo.references.map((column) =>
this.client.customWrapIdentifier(column, identity)
);
this.pushQuery({
sql: `PRAGMA table_info(${this.tableName()})`,
statementsProducer(pragma, connection) {
return compiler.client
.ddl(compiler, pragma, connection)
.foreign(foreignInfo);
},
});
}
}
primaryKeys() {
const pks = filter(this.grouped.alterTable || [], { method: 'primary' });
if (pks.length > 0 && pks[0].args.length > 0) {
const columns = pks[0].args[0];
let constraintName = pks[0].args[1] || '';
if (constraintName) {
constraintName = ' constraint ' + this.formatter.wrap(constraintName);
}
const needUniqueCols =
this.grouped.columns.filter((t) => t.builder._type === 'increments')
.length > 0;
// SQLite dont support autoincrement columns and composite primary keys (autoincrement is always primary key).
// You need to add unique index instead when you have autoincrement columns (https://stackoverflow.com/a/6154876/1535159)
return `,${constraintName} ${
needUniqueCols ? 'unique' : 'primary key'
} (${this.formatter.columnize(columns)})`;
}
}
foreignKeys() {
let sql = '';
const foreignKeys = filter(this.grouped.alterTable || [], {
method: 'foreign',
});
for (let i = 0, l = foreignKeys.length; i < l; i++) {
const foreign = foreignKeys[i].args[0];
const column = this.formatter.columnize(foreign.column);
const references = this.formatter.columnize(foreign.references);
const foreignTable = this.formatter.wrap(foreign.inTable);
let constraintName = foreign.keyName || '';
if (constraintName) {
constraintName = ' constraint ' + this.formatter.wrap(constraintName);
}
sql += `,${constraintName} foreign key(${column}) references ${foreignTable}(${references})`;
if (foreign.onDelete) sql += ` on delete ${foreign.onDelete}`;
if (foreign.onUpdate) sql += ` on update ${foreign.onUpdate}`;
}
return sql;
}
createTableBlock() {
return this.getColumns().concat().join(',');
}
renameColumn(from, to) {
this.pushQuery({
sql: `alter table ${this.tableName()} rename ${this.formatter.wrap(
from
)} to ${this.formatter.wrap(to)}`,
});
}
_setNullableState(column, isNullable) {
const compiler = this;
this.pushQuery({
sql: `PRAGMA table_info(${this.tableName()})`,
statementsProducer(pragma, connection) {
return compiler.client
.ddl(compiler, pragma, connection)
.setNullable(column, isNullable);
},
});
}
dropColumn() {
const compiler = this;
const columns = values(arguments);
const columnsWrapped = columns.map((column) =>
this.client.customWrapIdentifier(column, identity)
);
this.pushQuery({
sql: `PRAGMA table_info(${this.tableName()})`,
output(pragma) {
return compiler.client
.ddl(compiler, pragma, this.connection)
.dropColumn(columnsWrapped);
},
});
}
}
module.exports = TableCompiler_SQLite3;

View File

@ -0,0 +1,40 @@
/* eslint max-len: 0 */
const ViewCompiler = require('../../../schema/viewcompiler.js');
const {
columnize: columnize_,
} = require('../../../formatter/wrappingFormatter');
class ViewCompiler_SQLite3 extends ViewCompiler {
constructor(client, viewCompiler) {
super(client, viewCompiler);
}
createOrReplace() {
const columns = this.columns;
const selectQuery = this.selectQuery.toString();
const viewName = this.viewName();
const columnList = columns
? ' (' +
columnize_(
columns,
this.viewBuilder,
this.client,
this.bindingsHolder
) +
')'
: '';
const dropSql = `drop view if exists ${viewName}`;
const createSql = `create view ${viewName}${columnList} as ${selectQuery}`;
this.pushQuery({
sql: dropSql,
});
this.pushQuery({
sql: createSql,
});
}
}
module.exports = ViewCompiler_SQLite3;