@atscript/db-sqlite 0.1.146 → 0.1.148

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/dist/index.cjs CHANGED
@@ -78,7 +78,9 @@ var BetterSqlite3Driver = class {
78
78
  registerFunction(name, fn, opts) {
79
79
  this.db.function(name, { deterministic: opts?.deterministic ?? false }, fn);
80
80
  }
81
+ /** Idempotent: closing an already closed database is a no-op. */
81
82
  close() {
83
+ if (!this.db.open) return;
82
84
  this.db.close();
83
85
  }
84
86
  };
@@ -86,7 +88,8 @@ var BetterSqlite3Driver = class {
86
88
  //#region src/calendar-bucket.ts
87
89
  /**
88
90
  * Name of the scalar SQL function the adapter registers on its driver:
89
- * `atscript_bucket(value, unit, tz, weekStart)` → TEXT `'YYYY-MM-DD'` or NULL.
91
+ * `atscript_bucket(value, unit, tz, weekStart)` → TEXT `'YYYY-MM-DD'`
92
+ * (`'YYYY-MM-DDTHH:00'` for unit `hour`) or NULL.
90
93
  */
91
94
  const SQLITE_BUCKET_FN = "atscript_bucket";
92
95
  /**
@@ -287,6 +290,7 @@ const sqliteDialect = {
287
290
  },
288
291
  calendarBucket: sqliteCalendarBucket,
289
292
  jsonExtract: sqliteJsonExtract,
293
+ castDouble: (expr) => `CAST(${expr} AS REAL)`,
290
294
  createViewPrefix: "CREATE VIEW IF NOT EXISTS"
291
295
  };
292
296
  /**
@@ -438,8 +442,9 @@ function buildCreateTable(table, fields, foreignKeys, options) {
438
442
  colDefs.push(`PRIMARY KEY (${pkCols})`);
439
443
  }
440
444
  if (foreignKeys) for (const fk of foreignKeys.values()) {
441
- const localCols = fk.fields.map((f) => `"${esc(f)}"`).join(", ");
442
- const targetCols = fk.targetFields.map((f) => `"${esc(f)}"`).join(", ");
445
+ const { fields, targetFields } = (0, _atscript_db.fkColumns)(fk);
446
+ const localCols = fields.map((f) => `"${esc(f)}"`).join(", ");
447
+ const targetCols = targetFields.map((f) => `"${esc(f)}"`).join(", ");
443
448
  let constraint = `FOREIGN KEY (${localCols}) REFERENCES "${esc(fk.targetTable)}" (${targetCols})`;
444
449
  if (fk.onDelete) constraint += ` ON DELETE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
445
450
  if (fk.onUpdate) constraint += ` ON UPDATE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
@@ -452,23 +457,28 @@ function buildCreateTable(table, fields, foreignKeys, options) {
452
457
  /**
453
458
  * Translates a uniqu filter expression into a parameterized SQL WHERE clause.
454
459
  *
460
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
461
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
462
+ * source table name) — a statement with an aliased FROM must pass its alias.
463
+ *
464
+ * @param opts - since 0.1.147
455
465
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
456
466
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
457
467
  */
458
- function buildWhere(filter) {
459
- return (0, _atscript_db_sql_tools.buildWhere)(sqliteDialect, filter);
468
+ function buildWhere(filter, opts) {
469
+ return (0, _atscript_db_sql_tools.buildWhere)(sqliteDialect, filter, opts);
460
470
  }
461
471
  /**
462
472
  * Like {@link buildWhere} but prefixes all column references with a table alias.
463
473
  * Produces `alias."col"` instead of `"col"` — needed for JOINed queries (e.g. FTS5 search).
474
+ * Relational predicates correlate to the same alias (since 0.1.147); their
475
+ * own subquery aliases stay unprefixed.
464
476
  */
465
477
  function buildPrefixedWhere(alias, filter) {
466
- return (0, _atscript_db_sql_tools.buildWhere)({
467
- ...sqliteDialect,
468
- quoteIdentifier(name) {
469
- return `${alias}."${esc(name)}"`;
470
- }
471
- }, filter);
478
+ return (0, _atscript_db_sql_tools.buildWhere)(sqliteDialect, filter, {
479
+ columnRef: (name) => `${alias}."${esc(name)}"`,
480
+ qualifier: alias
481
+ });
472
482
  }
473
483
  //#endregion
474
484
  //#region src/tx-gate.ts
@@ -706,10 +716,18 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
706
716
  calendarBucketUnits() {
707
717
  return this._hasBucketFn ? _atscript_db.ALL_BUCKET_UNITS : super.calendarBucketUnits();
708
718
  }
709
- /** Every aggregate function, `countDistinct` included. */
719
+ /** Every aggregate function: `countDistinct`, `first` and `last` included. */
710
720
  aggregateFns() {
711
721
  return _atscript_db.ALL_AGGREGATE_FNS;
712
722
  }
723
+ /** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
724
+ supportsAggregateExpressions() {
725
+ return true;
726
+ }
727
+ /** Computed view columns and first-row joins. */
728
+ viewCapabilities() {
729
+ return _atscript_db.ALL_VIEW_CAPABILITIES;
730
+ }
713
731
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
714
732
  _supportsVector;
715
733
  /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
@@ -865,6 +883,15 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
865
883
  resolveTableName() {
866
884
  return super.resolveTableName(false);
867
885
  }
886
+ /**
887
+ * Relational predicates (`$some` / `$none`) render as correlated
888
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
889
+ *
890
+ * @since 0.1.147
891
+ */
892
+ supportsRelationFilters(_mode) {
893
+ return true;
894
+ }
868
895
  /** SQLite enforces FK constraints natively via PRAGMA foreign_keys. */
869
896
  supportsNativeForeignKeys() {
870
897
  return true;
@@ -919,6 +946,29 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
919
946
  };
920
947
  });
921
948
  }
949
+ supportsInsertIgnore() {
950
+ return true;
951
+ }
952
+ /**
953
+ * Per row `INSERT … ON CONFLICT DO NOTHING` (covers the primary key and every
954
+ * unique index; NOT NULL / CHECK / FK still raise — never `INSERT OR IGNORE`).
955
+ * `changes === 0` marks a skipped row.
956
+ */
957
+ async insertManyIgnore(data) {
958
+ return this.withTransaction(async () => {
959
+ const slots = [];
960
+ const tableName = this.resolveTableName();
961
+ for (const row of data) {
962
+ const built = buildInsert(tableName, row);
963
+ const sql = `${built.sql} ON CONFLICT DO NOTHING`;
964
+ const params = built.params;
965
+ this._log(sql, params);
966
+ const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
967
+ slots.push(result.changes === 0 ? null : { insertedId: this._resolveInsertedId(row, result.lastInsertRowid) });
968
+ }
969
+ return slots;
970
+ });
971
+ }
922
972
  async findOne(query) {
923
973
  const where = buildWhere(query.filter);
924
974
  const controls = {
@@ -935,6 +985,16 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
935
985
  this._log(sql, params);
936
986
  return this._stmt(() => this.driver.all(sql, params));
937
987
  }
988
+ /**
989
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
990
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
991
+ */
992
+ async findManyPerPartition(query, partitionBy) {
993
+ const where = buildWhere(query.filter);
994
+ const { sql, params } = (0, _atscript_db_sql_tools.buildPartitionedSelect)(sqliteDialect, this.resolveTableName(), where, query.controls, partitionBy);
995
+ this._log(sql, params);
996
+ return (0, _atscript_db_sql_tools.stripPartitionRowNumber)(await this._stmt(() => this.driver.all(sql, params)));
997
+ }
938
998
  async count(query) {
939
999
  const where = buildWhere(query.filter);
940
1000
  const sql = `SELECT COUNT(*) as cnt FROM "${esc(this.resolveTableName())}" WHERE ${where.sql}`;
@@ -1658,7 +1718,7 @@ var SqliteAdapter = class SqliteAdapter extends _atscript_db.BaseDbAdapter {
1658
1718
  const controls = query.controls ?? {};
1659
1719
  return {
1660
1720
  tableName: this.resolveTableName(),
1661
- where: buildWhere(query.filter),
1721
+ where: buildWhere(query.filter, { qualifier: sqliteDialect.quoteTable(_atscript_db_sql_tools.SEARCH_SOURCE_ALIAS) }),
1662
1722
  dist: haversineDistanceExpr(`"${esc(column)}"`, point),
1663
1723
  window: (0, _atscript_db_sql_tools.geoWindowFromControls)(controls),
1664
1724
  controls
@@ -1790,7 +1850,7 @@ function createAdapter(connection, options) {
1790
1850
  transactionWaitTimeoutMs,
1791
1851
  transactionWaitWarnMs
1792
1852
  };
1793
- return new _atscript_db.DbSpace(() => new SqliteAdapter(driver, adapterOptions));
1853
+ return new _atscript_db.DbSpace(() => new SqliteAdapter(driver, adapterOptions), { onClose: () => driver.close() });
1794
1854
  }
1795
1855
  //#endregion
1796
1856
  exports.BetterSqlite3Driver = BetterSqlite3Driver;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
2
2
  import { TMetadataMap } from "@atscript/typescript/utils";
3
3
  import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
4
- import { TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
4
+ import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
5
5
 
6
6
  //#region src/types.d.ts
7
7
  /**
@@ -121,6 +121,7 @@ declare class BetterSqlite3Driver implements TSqliteDriver {
121
121
  registerFunction(name: string, fn: (...args: any[]) => unknown, opts?: {
122
122
  deterministic?: boolean;
123
123
  }): void;
124
+ /** Idempotent: closing an already closed database is a no-op. */
124
125
  close(): void;
125
126
  }
126
127
  //#endregion
@@ -267,8 +268,12 @@ declare class SqliteAdapter extends BaseDbAdapter {
267
268
  * none otherwise (`BUCKET_NOT_SUPPORTED`, a clean 400).
268
269
  */
269
270
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
270
- /** Every aggregate function, `countDistinct` included. */
271
+ /** Every aggregate function: `countDistinct`, `first` and `last` included. */
271
272
  aggregateFns(): ReadonlySet<AggregateFn>;
273
+ /** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
274
+ supportsAggregateExpressions(): boolean;
275
+ /** Computed view columns and first-row joins. */
276
+ viewCapabilities(): ReadonlySet<TViewCapability>;
272
277
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
273
278
  private _supportsVector;
274
279
  /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
@@ -319,6 +324,13 @@ declare class SqliteAdapter extends BaseDbAdapter {
319
324
  protected _rollbackTransaction(state: unknown): Promise<void>;
320
325
  /** SQLite does not use schemas — override to always exclude schema. */
321
326
  resolveTableName(): string;
327
+ /**
328
+ * Relational predicates (`$some` / `$none`) render as correlated
329
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
330
+ *
331
+ * @since 0.1.147
332
+ */
333
+ supportsRelationFilters(_mode: "read" | "write"): boolean;
322
334
  /** SQLite enforces FK constraints natively via PRAGMA foreign_keys. */
323
335
  supportsNativeForeignKeys(): boolean;
324
336
  prepareId(id: unknown, _fieldType: unknown): unknown;
@@ -329,8 +341,20 @@ declare class SqliteAdapter extends BaseDbAdapter {
329
341
  private _wrapConstraintError;
330
342
  insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
331
343
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
344
+ supportsInsertIgnore(): boolean;
345
+ /**
346
+ * Per row `INSERT … ON CONFLICT DO NOTHING` (covers the primary key and every
347
+ * unique index; NOT NULL / CHECK / FK still raise — never `INSERT OR IGNORE`).
348
+ * `changes === 0` marks a skipped row.
349
+ */
350
+ insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
332
351
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
333
352
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
353
+ /**
354
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
355
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
356
+ */
357
+ findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
334
358
  count(query: DbQuery): Promise<number>;
335
359
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
336
360
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
@@ -488,10 +512,15 @@ declare class SqliteAdapter extends BaseDbAdapter {
488
512
  /**
489
513
  * Translates a uniqu filter expression into a parameterized SQL WHERE clause.
490
514
  *
515
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
516
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
517
+ * source table name) — a statement with an aliased FROM must pass its alias.
518
+ *
519
+ * @param opts - since 0.1.147
491
520
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
492
521
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
493
522
  */
494
- declare function buildWhere(filter: FilterExpr$1): TSqlFragment$1;
523
+ declare function buildWhere(filter: FilterExpr$1, opts?: TFilterVisitorOptions): TSqlFragment$1;
495
524
  //#endregion
496
525
  //#region src/index.d.ts
497
526
  /** `createAdapter` options: driver options plus the transaction-gate options (since 0.1.128). */
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair } from "@atscript/db";
2
- import { TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TFieldOps, TGenericLogger, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TValueFormatterPair, TViewCapability } from "@atscript/db";
2
+ import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
3
3
  import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
4
4
  import { TMetadataMap } from "@atscript/typescript/utils";
5
5
 
@@ -121,6 +121,7 @@ declare class BetterSqlite3Driver implements TSqliteDriver {
121
121
  registerFunction(name: string, fn: (...args: any[]) => unknown, opts?: {
122
122
  deterministic?: boolean;
123
123
  }): void;
124
+ /** Idempotent: closing an already closed database is a no-op. */
124
125
  close(): void;
125
126
  }
126
127
  //#endregion
@@ -267,8 +268,12 @@ declare class SqliteAdapter extends BaseDbAdapter {
267
268
  * none otherwise (`BUCKET_NOT_SUPPORTED`, a clean 400).
268
269
  */
269
270
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
270
- /** Every aggregate function, `countDistinct` included. */
271
+ /** Every aggregate function: `countDistinct`, `first` and `last` included. */
271
272
  aggregateFns(): ReadonlySet<AggregateFn>;
273
+ /** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
274
+ supportsAggregateExpressions(): boolean;
275
+ /** Computed view columns and first-row joins. */
276
+ viewCapabilities(): ReadonlySet<TViewCapability>;
272
277
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
273
278
  private _supportsVector;
274
279
  /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
@@ -319,6 +324,13 @@ declare class SqliteAdapter extends BaseDbAdapter {
319
324
  protected _rollbackTransaction(state: unknown): Promise<void>;
320
325
  /** SQLite does not use schemas — override to always exclude schema. */
321
326
  resolveTableName(): string;
327
+ /**
328
+ * Relational predicates (`$some` / `$none`) render as correlated
329
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
330
+ *
331
+ * @since 0.1.147
332
+ */
333
+ supportsRelationFilters(_mode: "read" | "write"): boolean;
322
334
  /** SQLite enforces FK constraints natively via PRAGMA foreign_keys. */
323
335
  supportsNativeForeignKeys(): boolean;
324
336
  prepareId(id: unknown, _fieldType: unknown): unknown;
@@ -329,8 +341,20 @@ declare class SqliteAdapter extends BaseDbAdapter {
329
341
  private _wrapConstraintError;
330
342
  insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
331
343
  insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
344
+ supportsInsertIgnore(): boolean;
345
+ /**
346
+ * Per row `INSERT … ON CONFLICT DO NOTHING` (covers the primary key and every
347
+ * unique index; NOT NULL / CHECK / FK still raise — never `INSERT OR IGNORE`).
348
+ * `changes === 0` marks a skipped row.
349
+ */
350
+ insertManyIgnore(data: Array<Record<string, unknown>>): Promise<TDbInsertIgnoreSlot[]>;
332
351
  findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
333
352
  findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
353
+ /**
354
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
355
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
356
+ */
357
+ findManyPerPartition(query: DbQuery, partitionBy: readonly string[]): Promise<Array<Record<string, unknown>>>;
334
358
  count(query: DbQuery): Promise<number>;
335
359
  aggregate(query: DbQuery): Promise<Array<Record<string, unknown>>>;
336
360
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
@@ -488,10 +512,15 @@ declare class SqliteAdapter extends BaseDbAdapter {
488
512
  /**
489
513
  * Translates a uniqu filter expression into a parameterized SQL WHERE clause.
490
514
  *
515
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
516
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
517
+ * source table name) — a statement with an aliased FROM must pass its alias.
518
+ *
519
+ * @param opts - since 0.1.147
491
520
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
492
521
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
493
522
  */
494
- declare function buildWhere(filter: FilterExpr$1): TSqlFragment$1;
523
+ declare function buildWhere(filter: FilterExpr$1, opts?: TFilterVisitorOptions): TSqlFragment$1;
495
524
  //#endregion
496
525
  //#region src/index.d.ts
497
526
  /** `createAdapter` options: driver options plus the transaction-gate options (since 0.1.128). */
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
- import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, searchIndexNotFoundMessage, vectorIndexNotFoundMessage } from "@atscript/db";
2
+ import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, fkColumns, searchIndexNotFoundMessage, vectorIndexNotFoundMessage } from "@atscript/db";
3
3
  import { resolveAggregateSearch } from "@atscript/db/agg";
4
- import { EMPTY_AND, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, geoWindowFromControls, jsonDollarPath, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, toSqlValue } from "@atscript/db-sql-tools";
4
+ import { EMPTY_AND, SEARCH_SOURCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, geoWindowFromControls, jsonDollarPath, parseRegexString, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue } from "@atscript/db-sql-tools";
5
5
  import { bucketer } from "@uniqu/core";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  //#region src/better-sqlite3-driver.ts
@@ -77,7 +77,9 @@ var BetterSqlite3Driver = class {
77
77
  registerFunction(name, fn, opts) {
78
78
  this.db.function(name, { deterministic: opts?.deterministic ?? false }, fn);
79
79
  }
80
+ /** Idempotent: closing an already closed database is a no-op. */
80
81
  close() {
82
+ if (!this.db.open) return;
81
83
  this.db.close();
82
84
  }
83
85
  };
@@ -85,7 +87,8 @@ var BetterSqlite3Driver = class {
85
87
  //#region src/calendar-bucket.ts
86
88
  /**
87
89
  * Name of the scalar SQL function the adapter registers on its driver:
88
- * `atscript_bucket(value, unit, tz, weekStart)` → TEXT `'YYYY-MM-DD'` or NULL.
90
+ * `atscript_bucket(value, unit, tz, weekStart)` → TEXT `'YYYY-MM-DD'`
91
+ * (`'YYYY-MM-DDTHH:00'` for unit `hour`) or NULL.
89
92
  */
90
93
  const SQLITE_BUCKET_FN = "atscript_bucket";
91
94
  /**
@@ -286,6 +289,7 @@ const sqliteDialect = {
286
289
  },
287
290
  calendarBucket: sqliteCalendarBucket,
288
291
  jsonExtract: sqliteJsonExtract,
292
+ castDouble: (expr) => `CAST(${expr} AS REAL)`,
289
293
  createViewPrefix: "CREATE VIEW IF NOT EXISTS"
290
294
  };
291
295
  /**
@@ -437,8 +441,9 @@ function buildCreateTable(table, fields, foreignKeys, options) {
437
441
  colDefs.push(`PRIMARY KEY (${pkCols})`);
438
442
  }
439
443
  if (foreignKeys) for (const fk of foreignKeys.values()) {
440
- const localCols = fk.fields.map((f) => `"${esc(f)}"`).join(", ");
441
- const targetCols = fk.targetFields.map((f) => `"${esc(f)}"`).join(", ");
444
+ const { fields, targetFields } = fkColumns(fk);
445
+ const localCols = fields.map((f) => `"${esc(f)}"`).join(", ");
446
+ const targetCols = targetFields.map((f) => `"${esc(f)}"`).join(", ");
442
447
  let constraint = `FOREIGN KEY (${localCols}) REFERENCES "${esc(fk.targetTable)}" (${targetCols})`;
443
448
  if (fk.onDelete) constraint += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
444
449
  if (fk.onUpdate) constraint += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
@@ -451,23 +456,28 @@ function buildCreateTable(table, fields, foreignKeys, options) {
451
456
  /**
452
457
  * Translates a uniqu filter expression into a parameterized SQL WHERE clause.
453
458
  *
459
+ * Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
460
+ * subqueries referencing the outer table by `opts.qualifier` (default: the
461
+ * source table name) — a statement with an aliased FROM must pass its alias.
462
+ *
463
+ * @param opts - since 0.1.147
454
464
  * @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
455
465
  * Returns `{ sql: '1=1', params: [] }` for empty/null filters.
456
466
  */
457
- function buildWhere(filter) {
458
- return buildWhere$1(sqliteDialect, filter);
467
+ function buildWhere(filter, opts) {
468
+ return buildWhere$1(sqliteDialect, filter, opts);
459
469
  }
460
470
  /**
461
471
  * Like {@link buildWhere} but prefixes all column references with a table alias.
462
472
  * Produces `alias."col"` instead of `"col"` — needed for JOINed queries (e.g. FTS5 search).
473
+ * Relational predicates correlate to the same alias (since 0.1.147); their
474
+ * own subquery aliases stay unprefixed.
463
475
  */
464
476
  function buildPrefixedWhere(alias, filter) {
465
- return buildWhere$1({
466
- ...sqliteDialect,
467
- quoteIdentifier(name) {
468
- return `${alias}."${esc(name)}"`;
469
- }
470
- }, filter);
477
+ return buildWhere$1(sqliteDialect, filter, {
478
+ columnRef: (name) => `${alias}."${esc(name)}"`,
479
+ qualifier: alias
480
+ });
471
481
  }
472
482
  //#endregion
473
483
  //#region src/tx-gate.ts
@@ -705,10 +715,18 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
705
715
  calendarBucketUnits() {
706
716
  return this._hasBucketFn ? ALL_BUCKET_UNITS : super.calendarBucketUnits();
707
717
  }
708
- /** Every aggregate function, `countDistinct` included. */
718
+ /** Every aggregate function: `countDistinct`, `first` and `last` included. */
709
719
  aggregateFns() {
710
720
  return ALL_AGGREGATE_FNS;
711
721
  }
722
+ /** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
723
+ supportsAggregateExpressions() {
724
+ return true;
725
+ }
726
+ /** Computed view columns and first-row joins. */
727
+ viewCapabilities() {
728
+ return ALL_VIEW_CAPABILITIES;
729
+ }
712
730
  /** Whether the SQLite connection has the sqlite-vec extension loaded. */
713
731
  _supportsVector;
714
732
  /** Whether SQLite math functions (radians/asin/...) are available — geo search. */
@@ -864,6 +882,15 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
864
882
  resolveTableName() {
865
883
  return super.resolveTableName(false);
866
884
  }
885
+ /**
886
+ * Relational predicates (`$some` / `$none`) render as correlated
887
+ * `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
888
+ *
889
+ * @since 0.1.147
890
+ */
891
+ supportsRelationFilters(_mode) {
892
+ return true;
893
+ }
867
894
  /** SQLite enforces FK constraints natively via PRAGMA foreign_keys. */
868
895
  supportsNativeForeignKeys() {
869
896
  return true;
@@ -918,6 +945,29 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
918
945
  };
919
946
  });
920
947
  }
948
+ supportsInsertIgnore() {
949
+ return true;
950
+ }
951
+ /**
952
+ * Per row `INSERT … ON CONFLICT DO NOTHING` (covers the primary key and every
953
+ * unique index; NOT NULL / CHECK / FK still raise — never `INSERT OR IGNORE`).
954
+ * `changes === 0` marks a skipped row.
955
+ */
956
+ async insertManyIgnore(data) {
957
+ return this.withTransaction(async () => {
958
+ const slots = [];
959
+ const tableName = this.resolveTableName();
960
+ for (const row of data) {
961
+ const built = buildInsert$1(tableName, row);
962
+ const sql = `${built.sql} ON CONFLICT DO NOTHING`;
963
+ const params = built.params;
964
+ this._log(sql, params);
965
+ const result = await this._stmt(() => this._wrapConstraintError(() => this.driver.run(sql, params)));
966
+ slots.push(result.changes === 0 ? null : { insertedId: this._resolveInsertedId(row, result.lastInsertRowid) });
967
+ }
968
+ return slots;
969
+ });
970
+ }
921
971
  async findOne(query) {
922
972
  const where = buildWhere(query.filter);
923
973
  const controls = {
@@ -934,6 +984,16 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
934
984
  this._log(sql, params);
935
985
  return this._stmt(() => this.driver.all(sql, params));
936
986
  }
987
+ /**
988
+ * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
989
+ * window over `partitionBy` (the generic `$with` loader's per-parent page).
990
+ */
991
+ async findManyPerPartition(query, partitionBy) {
992
+ const where = buildWhere(query.filter);
993
+ const { sql, params } = buildPartitionedSelect(sqliteDialect, this.resolveTableName(), where, query.controls, partitionBy);
994
+ this._log(sql, params);
995
+ return stripPartitionRowNumber(await this._stmt(() => this.driver.all(sql, params)));
996
+ }
937
997
  async count(query) {
938
998
  const where = buildWhere(query.filter);
939
999
  const sql = `SELECT COUNT(*) as cnt FROM "${esc(this.resolveTableName())}" WHERE ${where.sql}`;
@@ -1657,7 +1717,7 @@ var SqliteAdapter = class SqliteAdapter extends BaseDbAdapter {
1657
1717
  const controls = query.controls ?? {};
1658
1718
  return {
1659
1719
  tableName: this.resolveTableName(),
1660
- where: buildWhere(query.filter),
1720
+ where: buildWhere(query.filter, { qualifier: sqliteDialect.quoteTable(SEARCH_SOURCE_ALIAS) }),
1661
1721
  dist: haversineDistanceExpr(`"${esc(column)}"`, point),
1662
1722
  window: geoWindowFromControls(controls),
1663
1723
  controls
@@ -1789,7 +1849,7 @@ function createAdapter(connection, options) {
1789
1849
  transactionWaitTimeoutMs,
1790
1850
  transactionWaitWarnMs
1791
1851
  };
1792
- return new DbSpace(() => new SqliteAdapter(driver, adapterOptions));
1852
+ return new DbSpace(() => new SqliteAdapter(driver, adapterOptions), { onClose: () => driver.close() });
1793
1853
  }
1794
1854
  //#endregion
1795
1855
  export { BetterSqlite3Driver, SqliteAdapter, SqliteTxGate, SqliteTxState, buildWhere, createAdapter, getSqliteTxGate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-sqlite",
3
- "version": "0.1.146",
3
+ "version": "0.1.148",
4
4
  "description": "SQLite adapter for @atscript/db with swappable driver support.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -36,21 +36,21 @@
36
36
  "access": "public"
37
37
  },
38
38
  "devDependencies": {
39
- "@atscript/core": "^0.1.98",
40
- "@atscript/typescript": "^0.1.98",
39
+ "@atscript/core": "^0.1.100",
40
+ "@atscript/typescript": "^0.1.100",
41
41
  "@types/better-sqlite3": "^7.6.13",
42
- "@uniqu/core": "^0.1.11",
42
+ "@uniqu/core": "^0.1.13",
43
43
  "better-sqlite3": "^12.6.2",
44
44
  "sqlite-vec": "^0.1.9",
45
- "unplugin-atscript": "^0.1.98"
45
+ "unplugin-atscript": "^0.1.100"
46
46
  },
47
47
  "peerDependencies": {
48
- "@atscript/core": "^0.1.98",
49
- "@atscript/typescript": "^0.1.98",
50
- "@uniqu/core": "^0.1.11",
48
+ "@atscript/core": "^0.1.100",
49
+ "@atscript/typescript": "^0.1.100",
50
+ "@uniqu/core": "^0.1.13",
51
51
  "better-sqlite3": ">=11.0.0",
52
- "@atscript/db": "^0.1.146",
53
- "@atscript/db-sql-tools": "^0.1.146"
52
+ "@atscript/db": "^0.1.148",
53
+ "@atscript/db-sql-tools": "^0.1.148"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "better-sqlite3": {