Skip to content

Commit

Permalink
Initial work, needs polish
Browse files Browse the repository at this point in the history
  • Loading branch information
code-ape committed Dec 29, 2023
1 parent 63e251b commit b35e0f1
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 48 deletions.
1 change: 1 addition & 0 deletions lib/.gitignore
Expand Up @@ -7,4 +7,5 @@
**/*.js.map

# Do not include .js files from .ts files
util/version.js
dialects/index.js
23 changes: 23 additions & 0 deletions lib/dialects/sqlite3/index.js
Expand Up @@ -16,6 +16,7 @@ const ViewCompiler = require('./schema/sqlite-viewcompiler');
const SQLite3_DDL = require('./schema/ddl');
const Formatter = require('../../formatter');
const QueryBuilder = require('./query/sqlite-querybuilder');
const { parseVersion, compareVersions } = require('../../util/version');

class Client_SQLite3 extends Client {
constructor(config) {
Expand All @@ -33,6 +34,28 @@ class Client_SQLite3 extends Client {
return require('sqlite3');
}

/**
* Get `Version` from client or return `[0, 0, 0]`
*
* @returns {[number, number, number]}
*/
_clientVersion() {
const version = parseVersion(this.client.VERSION);
if (!version) return [0, 0, 0];
return version;
}

/**
* Takes min, max, or both and returns if given client satisfies version
*
* @param {undefined | [number, number, number]} minVersion
* @param {undefined | [number, number, number]} maxVersion
* @returns {boolean}
*/
_satisfiesVersion(minVersion, maxVersion) {
return compareVersions(this._clientVersion(), minVersion, maxVersion);
}

schemaCompiler() {
return new SchemaCompiler(this, ...arguments);
}
Expand Down
70 changes: 22 additions & 48 deletions lib/dialects/sqlite3/query/sqlite-querycompiler.js
Expand Up @@ -34,6 +34,7 @@ class QueryCompiler_SQLite3 extends QueryCompiler {
const insertValues = this.single.insert || [];
let sql = this.with() + `insert into ${this.tableName} `;

// Handle "empty cases" of: [], [{}], {}
if (Array.isArray(insertValues)) {
if (insertValues.length === 0) {
return '';
Expand Down Expand Up @@ -66,6 +67,7 @@ class QueryCompiler_SQLite3 extends QueryCompiler {
};
}

// 'insert into TABLE_NAME (column1, column2, ...)'
sql += `(${this.formatter.columnize(insertData.columns)})`;

// backwards compatible error
Expand All @@ -82,62 +84,34 @@ class QueryCompiler_SQLite3 extends QueryCompiler {
});
}

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,
};
if (
!this.client._satisfiesVersion([3, 7, 11]) &&
insertData.values.length > 1
) {
throw new Error('Requires Sqlite 3.7.11 or newer to do multi-insert');
}

const blocks = [];
// Holds parameters wrapped in `()`
const parametersArray = [];
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(', ');
const parameter = this.client.parameterize(
insertData.values[i],
this.client.valueForUndefined,
this.builder,
this.bindingsHolder
);
parametersArray.push(`(${parameter})`);
}
sql += ' select ' + blocks.join(' union all select ');
// 'insert into TABLE_NAME (column1, column2, ...) values (v1, v2, ...), (v3, v4, ...), ...'
sql += ` values (${parametersArray.join(', ')})`;

const { onConflict, ignore, merge } = this.single;
if (onConflict && ignore) sql += ' where true' + this._ignore(onConflict);
if (onConflict && ignore) sql += this._ignore(onConflict);
else if (onConflict && merge) {
sql +=
' where true' + this._merge(merge.updates, onConflict, insertValues);
sql += this._merge(merge.updates, onConflict, insertValues);
const wheres = this.where();
if (wheres) sql += ` ${wheres}`;
}

const { returning } = this.single;
Expand Down
24 changes: 24 additions & 0 deletions lib/query/querycompiler.js
Expand Up @@ -1260,6 +1260,18 @@ class QueryCompiler {
return str;
}

/**
* TODO
*
* Takes:
*
* - Single object or array of objects.
*
* Does:
*
* - If a raw value is given return it.
* - Else returns `{ columns, values }` object
*/
_prepInsert(data) {
const isRaw = rawOrFn_(
data,
Expand All @@ -1273,24 +1285,36 @@ class QueryCompiler {
const values = [];
if (!Array.isArray(data)) data = data ? [data] : [];
let i = -1;
// Iterate over each item given ...
while (++i < data.length) {
// ... ignoring null values ...
if (data[i] == null) break;
// ... if this is the first item populate `columns` from its sorted keys ...
if (i === 0) columns = Object.keys(data[i]).sort();
// ... create row value array based on `columns` ...
const row = new Array(columns.length);
// ... populate `keys` with items keys ...
const keys = Object.keys(data[i]);
// ... then for each key ...
let j = -1;
while (++j < keys.length) {
const key = keys[j];
// ... attempt to get the `column` index that key goes to ...
let idx = columns.indexOf(key);
// ... if no index found ...
if (idx === -1) {
// ... then retro-actively update existing columns and existing `values`
// to incorporate newly discovered key ...
columns = columns.concat(key).sort();
idx = columns.indexOf(key);
let k = -1;
while (++k < values.length) {
values[k].splice(idx, 0, undefined);
}
// ... and then update row being actively built.
row.splice(idx, 0, undefined);
}
// ... else if column index found populate it with value.
row[idx] = data[i][key];
}
values.push(row);
Expand Down
95 changes: 95 additions & 0 deletions lib/util/version.ts
@@ -0,0 +1,95 @@

/** Tuple of three non-negative integers representing a semantic version */
export type Version = [number, number, number]

/** Helper function to check `x` a integer where `x >= 0` */
function isNonNegInt(x: unknown): x is number {
return typeof x === 'number' && Number.isInteger(x) && x >= 0
}

/** Type-guard for `Version` type */
export function isVersion(x: unknown): x is Version {
return Array.isArray(x) &&
x.length === 3 &&
x.findIndex((y) => !isNonNegInt(y)) === -1
}

/** Parses given string into `Version` or returns `undefined` */
export function parseVersion(x: string): Version | undefined {
const versionRegex = /^(\d+)\.(\d+)\.(\d+)/m
const versionNumbers = (versionRegex.exec(x) ?? []).slice(1, 4)
if (!isVersion(versionNumbers)) return undefined
return versionNumbers
}

/** Parses given string into `Version` or throws an error */
export function parseVersionOrError(x: string): Version {
const version = parseVersion(x)
if (version === undefined) {
throw new Error('Could not parse string to Version')
}
return version
}

/**
* Compares two versions, returning a number to represent the result:
*
* - `1` means `v1 > v2`
* - `0` means `v1 == v2`
* - `-1` means `v1 < v2`
*/
export function compareVersions(v1: Version, v2: Version): 1 | 0 | -1 {
// Check major
if (v1[0] < v2[0]) return -1
else if (v1[0] > v2[0]) return 1
else {
// Check minor
if (v1[1] < v2[1]) return -1
else if (v1[1] > v2[1]) return 1
else {
// Check patch
if (v1[2] < v2[2]) return -1
else if (v1[2] > v2[2]) return 1
else return 0
}
}
}

/**
* Returns `boolean` for if a given `version` satisfies the given `min` and `max`.
*
* This will throw an error if:
*
* - Given `version` is NOT a valid `Version`
* - Neither `min` nor `max` is given
* - `min` is given but is NOT a valid `Version`
* - `max` is given but is NOT a valid `Version`
*/
export function satisfiesVersion(version: Version, min?: Version, max?: Version): boolean {
if (!min && !max) {
throw new Error('Must pass at least one version constraint')
}
if (!isVersion(version)) {
throw new Error('Invalid value given for: version')
}

// Check Min
let satisfiesMin = true
if (min) {
if (!isVersion(min)) {
throw new Error('Invalid value given for: min')
}
satisfiesMin = compareVersions(version, min) > -1
}

// Check max
let satisfiesMax = true
if (max) {
if (!isVersion(max)) {
throw new Error('Invalid value given for: max')
}
satisfiesMax = compareVersions(version, max) === -1
}

return satisfiesMin && satisfiesMax
}

0 comments on commit b35e0f1

Please sign in to comment.