@lunora/sql-store 1.0.0-alpha.5 → 1.0.0-alpha.51

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/LICENSE.md CHANGED
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
103
103
  under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
104
  CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
105
  specific language governing permissions and limitations under the License.
106
+
107
+ <!-- DEPENDENCIES -->
108
+ <!-- /DEPENDENCIES -->
109
+
110
+ <!-- TYPE_DEPENDENCIES -->
111
+ <!-- /TYPE_DEPENDENCIES -->
@@ -5,18 +5,18 @@ interface SqlRunResult {
5
5
  rowsAffected: number;
6
6
  }
7
7
  /**
8
- * The async SQL surface the store core consumes. Satisfied by a
9
- * `D1Session`/`D1Client` (D1), a `node:sqlite` adapter (tests), or a
10
- * Hyperdrive-backed `postgres`/`pg`/`mysql2` driver (PlanetScale).
11
- *
12
- * `all` runs a row-returning statement (incl. `... RETURNING ...`); `run` runs a
13
- * write and reports `rowsAffected` — the matched-rows count that drives the
14
- * MySQL optimistic-concurrency guard (which has no `RETURNING`).
15
- *
16
- * Note: companion writes (aggregate/rank/FTS/CDC) run as separate sequential
17
- * statements after the row write on every engine, so they share D1's
18
- * at-least-once caveat — there is no cross-statement transaction here.
19
- */
8
+ * The async SQL surface the store core consumes. Satisfied by a
9
+ * `D1Session`/`D1Client` (D1), a `node:sqlite` adapter (tests), or a
10
+ * Hyperdrive-backed `postgres`/`pg`/`mysql2` driver (PlanetScale).
11
+ *
12
+ * `all` runs a row-returning statement (incl. `... RETURNING ...`); `run` runs a
13
+ * write and reports `rowsAffected` — the matched-rows count that drives the
14
+ * MySQL optimistic-concurrency guard (which has no `RETURNING`).
15
+ *
16
+ * Note: companion writes (aggregate/rank/FTS/CDC) run as separate sequential
17
+ * statements after the row write on every engine, so they share D1's
18
+ * at-least-once caveat — there is no cross-statement transaction here.
19
+ */
20
20
  interface SqlExec {
21
21
  all: (sql: string, params: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
22
22
  run: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRunResult>;
@@ -26,19 +26,19 @@ interface SqlDialect {
26
26
  /** Affected-rows extractor for the OCC fallback when `supportsReturning` is false (MySQL). */
27
27
  affectedRows?: (result: SqlRunResult) => number;
28
28
  /**
29
- * Storage SQL column type for a validator `kind`. SQLite affinity
30
- * (`TEXT`/`INTEGER`/`REAL`/`BLOB`); Postgres `TEXT`/`DOUBLE PRECISION`/
31
- * `BOOLEAN`/`JSONB`/`BYTEA`; MySQL `VARCHAR(255)`/`TEXT`/`DOUBLE`/
32
- * `TINYINT(1)`/`JSON`/`LONGBLOB`.
33
- */
29
+ * Storage SQL column type for a validator `kind`. SQLite affinity
30
+ * (`TEXT`/`INTEGER`/`REAL`/`BLOB`); Postgres `TEXT`/`DOUBLE PRECISION`/
31
+ * `BOOLEAN`/`JSONB`/`BYTEA`; MySQL `VARCHAR(255)`/`TEXT`/`DOUBLE`/
32
+ * `TINYINT(1)`/`JSON`/`LONGBLOB`.
33
+ */
34
34
  columnType: (kind: string | undefined) => string;
35
35
  /**
36
- * Engine SQL types for the **internal companion tables** (aggregate / rank /
37
- * CDC), which are built from raw SQL types, not validator kinds. SQLite uses
38
- * `TEXT`/`REAL`/`INTEGER`/`BLOB` and `INTEGER PRIMARY KEY AUTOINCREMENT`;
39
- * Postgres `TEXT`/`DOUBLE PRECISION`/`INTEGER`/`BYTEA` + `BIGSERIAL`; MySQL
40
- * needs a bounded `VARCHAR` key, `DOUBLE`, `LONGBLOB`, `AUTO_INCREMENT`.
41
- */
36
+ * Engine SQL types for the **internal companion tables** (aggregate / rank /
37
+ * CDC), which are built from raw SQL types, not validator kinds. SQLite uses
38
+ * `TEXT`/`REAL`/`INTEGER`/`BLOB` and `INTEGER PRIMARY KEY AUTOINCREMENT`;
39
+ * Postgres `TEXT`/`DOUBLE PRECISION`/`INTEGER`/`BYTEA` + `BIGSERIAL`; MySQL
40
+ * needs a bounded `VARCHAR` key, `DOUBLE`, `LONGBLOB`, `AUTO_INCREMENT`.
41
+ */
42
42
  companionTypes: {
43
43
  autoincrementPrimaryKey: string;
44
44
  integer: string;
@@ -46,9 +46,19 @@ interface SqlDialect {
46
46
  real: string;
47
47
  text: string;
48
48
  };
49
- /** Map a stored value back to its JS form, by effective validator `kind` (inverse of `encode`). */
49
+ /**
50
+ * Map a stored value back to its JS form, by effective validator `kind`
51
+ * (inverse of `encode`). NOTE: currently **unused** by the store core, which
52
+ * hard-codes `sqliteDecode` in `decodeGlobalRow` on every engine. Kept on the
53
+ * seam for a future engine-native codec; an override here does not run today.
54
+ */
50
55
  decode: (value: unknown, kind: string | undefined) => unknown;
51
- /** Map a JS value to its bound storage form (boolean→1/0, bigint→string, object→JSON on SQLite; mostly native on PG). */
56
+ /**
57
+ * Map a JS value to its bound storage form (boolean→1/0, bigint→string,
58
+ * object→JSON on SQLite). NOTE: currently **unused** by the store core, which
59
+ * hard-codes `sqliteEncode` as `serializeColumnValue` on every engine. Kept on
60
+ * the seam for a future engine-native codec; an override here does not run today.
61
+ */
52
62
  encode: (value: unknown) => unknown;
53
63
  /** The framework columns every global table carries — the `id` primary key and `_creationTime` — as `{ name, type }` so the DDL builder can quote each name through the engine's dialect. */
54
64
  frameworkColumns: () => ReadonlyArray<{
@@ -56,27 +66,79 @@ interface SqlDialect {
56
66
  type: string;
57
67
  }>;
58
68
  /**
59
- * Optional: the key-prefix length an indexed column of this `kind` needs.
60
- * MySQL/InnoDB can't index a `TEXT`/`LONGTEXT`/`BLOB` column without a prefix
61
- * (the store appends `(&lt;n>)` to the column reference); SQLite/Postgres index
62
- * text columns directly and omit this hook (or return `undefined`). `kind` is
63
- * the column's effective validator kind.
64
- */
69
+ * Optional: the key-prefix length an indexed column of this `kind` needs.
70
+ * MySQL/InnoDB can't index a `TEXT`/`LONGTEXT`/`BLOB` column without a prefix
71
+ * (the store appends `(&lt;n>)` to the column reference); SQLite/Postgres index
72
+ * text columns directly and omit this hook (or return `undefined`). `kind` is
73
+ * the column's effective validator kind.
74
+ */
65
75
  indexKeyPrefix?: (kind: string | undefined) => number | undefined;
66
76
  /** True when an `error` thrown by a write is a UNIQUE-constraint breach (mapped to a 409 ConflictError). */
67
77
  isUniqueViolation: (error: unknown) => boolean;
68
78
  /** A short engine tag for diagnostics/branching (`"sqlite" | "postgres" | "mysql"`). The store core selects drizzle's matching dialect for rendering off this. */
69
79
  readonly name: "mysql" | "postgres" | "sqlite";
80
+ /**
81
+ * Optional: the engine's own full-text index, opted into per search index
82
+ * with `.searchIndex({ strategy: "native" })`.
83
+ *
84
+ * Only Postgres supplies one today (`tsvector` + GIN + `to_tsquery`). It
85
+ * scales sublinearly where the portable inverted companion aggregates every
86
+ * matching token row, but it ranks with the engine's formula rather than the
87
+ * shared scorer — which is why it is opt-in and why the parity suite asserts
88
+ * matching, not order, for indexes that use it.
89
+ *
90
+ * Recall still matches the portable path: the stored form is built from the
91
+ * tokens Lunora's analyzer already produced, under a configuration that adds
92
+ * no stemming or stopwords of the engine's own.
93
+ *
94
+ * Every member returns a *statement*, not a fragment, so no engine grammar
95
+ * reaches the store core: Postgres matches with `@@` against a `tsvector`
96
+ * column while MySQL would use `MATCH … AGAINST` against a text column, and
97
+ * both fit here without the caller knowing which.
98
+ */
99
+ nativeTextSearch?: {
100
+ /** DDL for the companion table holding the engine's indexed form, keyed by document id. */
101
+ createCompanion: (companion: string, keyType: string) => SQL;
102
+ /** DDL for the indexes that make the match fast. */
103
+ createIndexes: (companion: string) => SQL[];
104
+ /** Replace one document's row, given its already-analyzed token stream. */
105
+ indexDocument: (companion: string, id: string, analyzed: string) => SQL;
106
+ /** The `WHERE` predicate matching a query's analyzed terms, final term as a prefix. */
107
+ matches: (companion: string, terms: ReadonlyArray<string>) => SQL;
108
+ /** The `ORDER BY` expression, best first. */
109
+ rank: (companion: string, terms: ReadonlyArray<string>) => SQL;
110
+ };
111
+ /**
112
+ * True when the engine ships SQLite's FTS5 module, which decides whether a
113
+ * search index is stored as an FTS5 shadow or as the portable inverted
114
+ * companion.
115
+ *
116
+ * A static property of the engine, so it is declared rather than probed:
117
+ * the previous `CREATE VIRTUAL TABLE` capability probe spent a round trip
118
+ * (and an error in the database's log) on every fresh connection to
119
+ * rediscover something the dialect already knows. Tests that want the
120
+ * portable layout override this instead of intercepting SQL strings.
121
+ */
122
+ supportsFts5: boolean;
70
123
  /** True when the engine supports `UPDATE/DELETE ... RETURNING` (SQLite/PG yes, MySQL no → use `affectedRows`). */
71
124
  supportsReturning: boolean;
72
125
  /**
73
- * The catalog probe for whether a physical `table` exists — backs the opt-in
74
- * companion-table (`__agg_`/`__rank_`) existence checks. Returns a drizzle
75
- * {@link SQL} (a non-empty result ⇒ the table exists) so it renders through
76
- * the same per-engine path as every other statement, never a hand-built
77
- * placeholder string. SQLite reads `sqlite_master`; Postgres/MySQL read
78
- * `information_schema.tables`.
79
- */
126
+ * The catalog probe for whether a physical `table` exists — backs the opt-in
127
+ * companion-table (`__agg_`/`__rank_`) existence checks. Returns a drizzle
128
+ * {@link SQL} (a non-empty result ⇒ the table exists) so it renders through
129
+ * the same per-engine path as every other statement, never a hand-built
130
+ * placeholder string. SQLite reads `sqlite_master`; Postgres/MySQL read
131
+ * `information_schema.tables`.
132
+ */
80
133
  tableExists: (table: string) => SQL;
134
+ /**
135
+ * Optional: btree operator class appended to an indexed text column so a
136
+ * `LIKE 'prefix%'` scan can use the index whatever the database collation.
137
+ * Postgres needs `text_pattern_ops` (a default `text_ops` btree built under
138
+ * e.g. `en_US.UTF-8` is useless to `LIKE`); SQLite and MySQL index prefix
139
+ * matches off the plain index and omit this. Only the search companion's
140
+ * token index reads it.
141
+ */
142
+ textPatternOperatorClass?: string;
81
143
  }
82
144
  export { SqlDialect, SqlExec, SqlRunResult };
package/dist/dialect.d.ts CHANGED
@@ -5,18 +5,18 @@ interface SqlRunResult {
5
5
  rowsAffected: number;
6
6
  }
7
7
  /**
8
- * The async SQL surface the store core consumes. Satisfied by a
9
- * `D1Session`/`D1Client` (D1), a `node:sqlite` adapter (tests), or a
10
- * Hyperdrive-backed `postgres`/`pg`/`mysql2` driver (PlanetScale).
11
- *
12
- * `all` runs a row-returning statement (incl. `... RETURNING ...`); `run` runs a
13
- * write and reports `rowsAffected` — the matched-rows count that drives the
14
- * MySQL optimistic-concurrency guard (which has no `RETURNING`).
15
- *
16
- * Note: companion writes (aggregate/rank/FTS/CDC) run as separate sequential
17
- * statements after the row write on every engine, so they share D1's
18
- * at-least-once caveat — there is no cross-statement transaction here.
19
- */
8
+ * The async SQL surface the store core consumes. Satisfied by a
9
+ * `D1Session`/`D1Client` (D1), a `node:sqlite` adapter (tests), or a
10
+ * Hyperdrive-backed `postgres`/`pg`/`mysql2` driver (PlanetScale).
11
+ *
12
+ * `all` runs a row-returning statement (incl. `... RETURNING ...`); `run` runs a
13
+ * write and reports `rowsAffected` — the matched-rows count that drives the
14
+ * MySQL optimistic-concurrency guard (which has no `RETURNING`).
15
+ *
16
+ * Note: companion writes (aggregate/rank/FTS/CDC) run as separate sequential
17
+ * statements after the row write on every engine, so they share D1's
18
+ * at-least-once caveat — there is no cross-statement transaction here.
19
+ */
20
20
  interface SqlExec {
21
21
  all: (sql: string, params: ReadonlyArray<unknown>) => Promise<Record<string, unknown>[]>;
22
22
  run: (sql: string, params: ReadonlyArray<unknown>) => Promise<SqlRunResult>;
@@ -26,19 +26,19 @@ interface SqlDialect {
26
26
  /** Affected-rows extractor for the OCC fallback when `supportsReturning` is false (MySQL). */
27
27
  affectedRows?: (result: SqlRunResult) => number;
28
28
  /**
29
- * Storage SQL column type for a validator `kind`. SQLite affinity
30
- * (`TEXT`/`INTEGER`/`REAL`/`BLOB`); Postgres `TEXT`/`DOUBLE PRECISION`/
31
- * `BOOLEAN`/`JSONB`/`BYTEA`; MySQL `VARCHAR(255)`/`TEXT`/`DOUBLE`/
32
- * `TINYINT(1)`/`JSON`/`LONGBLOB`.
33
- */
29
+ * Storage SQL column type for a validator `kind`. SQLite affinity
30
+ * (`TEXT`/`INTEGER`/`REAL`/`BLOB`); Postgres `TEXT`/`DOUBLE PRECISION`/
31
+ * `BOOLEAN`/`JSONB`/`BYTEA`; MySQL `VARCHAR(255)`/`TEXT`/`DOUBLE`/
32
+ * `TINYINT(1)`/`JSON`/`LONGBLOB`.
33
+ */
34
34
  columnType: (kind: string | undefined) => string;
35
35
  /**
36
- * Engine SQL types for the **internal companion tables** (aggregate / rank /
37
- * CDC), which are built from raw SQL types, not validator kinds. SQLite uses
38
- * `TEXT`/`REAL`/`INTEGER`/`BLOB` and `INTEGER PRIMARY KEY AUTOINCREMENT`;
39
- * Postgres `TEXT`/`DOUBLE PRECISION`/`INTEGER`/`BYTEA` + `BIGSERIAL`; MySQL
40
- * needs a bounded `VARCHAR` key, `DOUBLE`, `LONGBLOB`, `AUTO_INCREMENT`.
41
- */
36
+ * Engine SQL types for the **internal companion tables** (aggregate / rank /
37
+ * CDC), which are built from raw SQL types, not validator kinds. SQLite uses
38
+ * `TEXT`/`REAL`/`INTEGER`/`BLOB` and `INTEGER PRIMARY KEY AUTOINCREMENT`;
39
+ * Postgres `TEXT`/`DOUBLE PRECISION`/`INTEGER`/`BYTEA` + `BIGSERIAL`; MySQL
40
+ * needs a bounded `VARCHAR` key, `DOUBLE`, `LONGBLOB`, `AUTO_INCREMENT`.
41
+ */
42
42
  companionTypes: {
43
43
  autoincrementPrimaryKey: string;
44
44
  integer: string;
@@ -46,9 +46,19 @@ interface SqlDialect {
46
46
  real: string;
47
47
  text: string;
48
48
  };
49
- /** Map a stored value back to its JS form, by effective validator `kind` (inverse of `encode`). */
49
+ /**
50
+ * Map a stored value back to its JS form, by effective validator `kind`
51
+ * (inverse of `encode`). NOTE: currently **unused** by the store core, which
52
+ * hard-codes `sqliteDecode` in `decodeGlobalRow` on every engine. Kept on the
53
+ * seam for a future engine-native codec; an override here does not run today.
54
+ */
50
55
  decode: (value: unknown, kind: string | undefined) => unknown;
51
- /** Map a JS value to its bound storage form (boolean→1/0, bigint→string, object→JSON on SQLite; mostly native on PG). */
56
+ /**
57
+ * Map a JS value to its bound storage form (boolean→1/0, bigint→string,
58
+ * object→JSON on SQLite). NOTE: currently **unused** by the store core, which
59
+ * hard-codes `sqliteEncode` as `serializeColumnValue` on every engine. Kept on
60
+ * the seam for a future engine-native codec; an override here does not run today.
61
+ */
52
62
  encode: (value: unknown) => unknown;
53
63
  /** The framework columns every global table carries — the `id` primary key and `_creationTime` — as `{ name, type }` so the DDL builder can quote each name through the engine's dialect. */
54
64
  frameworkColumns: () => ReadonlyArray<{
@@ -56,27 +66,79 @@ interface SqlDialect {
56
66
  type: string;
57
67
  }>;
58
68
  /**
59
- * Optional: the key-prefix length an indexed column of this `kind` needs.
60
- * MySQL/InnoDB can't index a `TEXT`/`LONGTEXT`/`BLOB` column without a prefix
61
- * (the store appends `(&lt;n>)` to the column reference); SQLite/Postgres index
62
- * text columns directly and omit this hook (or return `undefined`). `kind` is
63
- * the column's effective validator kind.
64
- */
69
+ * Optional: the key-prefix length an indexed column of this `kind` needs.
70
+ * MySQL/InnoDB can't index a `TEXT`/`LONGTEXT`/`BLOB` column without a prefix
71
+ * (the store appends `(&lt;n>)` to the column reference); SQLite/Postgres index
72
+ * text columns directly and omit this hook (or return `undefined`). `kind` is
73
+ * the column's effective validator kind.
74
+ */
65
75
  indexKeyPrefix?: (kind: string | undefined) => number | undefined;
66
76
  /** True when an `error` thrown by a write is a UNIQUE-constraint breach (mapped to a 409 ConflictError). */
67
77
  isUniqueViolation: (error: unknown) => boolean;
68
78
  /** A short engine tag for diagnostics/branching (`"sqlite" | "postgres" | "mysql"`). The store core selects drizzle's matching dialect for rendering off this. */
69
79
  readonly name: "mysql" | "postgres" | "sqlite";
80
+ /**
81
+ * Optional: the engine's own full-text index, opted into per search index
82
+ * with `.searchIndex({ strategy: "native" })`.
83
+ *
84
+ * Only Postgres supplies one today (`tsvector` + GIN + `to_tsquery`). It
85
+ * scales sublinearly where the portable inverted companion aggregates every
86
+ * matching token row, but it ranks with the engine's formula rather than the
87
+ * shared scorer — which is why it is opt-in and why the parity suite asserts
88
+ * matching, not order, for indexes that use it.
89
+ *
90
+ * Recall still matches the portable path: the stored form is built from the
91
+ * tokens Lunora's analyzer already produced, under a configuration that adds
92
+ * no stemming or stopwords of the engine's own.
93
+ *
94
+ * Every member returns a *statement*, not a fragment, so no engine grammar
95
+ * reaches the store core: Postgres matches with `@@` against a `tsvector`
96
+ * column while MySQL would use `MATCH … AGAINST` against a text column, and
97
+ * both fit here without the caller knowing which.
98
+ */
99
+ nativeTextSearch?: {
100
+ /** DDL for the companion table holding the engine's indexed form, keyed by document id. */
101
+ createCompanion: (companion: string, keyType: string) => SQL;
102
+ /** DDL for the indexes that make the match fast. */
103
+ createIndexes: (companion: string) => SQL[];
104
+ /** Replace one document's row, given its already-analyzed token stream. */
105
+ indexDocument: (companion: string, id: string, analyzed: string) => SQL;
106
+ /** The `WHERE` predicate matching a query's analyzed terms, final term as a prefix. */
107
+ matches: (companion: string, terms: ReadonlyArray<string>) => SQL;
108
+ /** The `ORDER BY` expression, best first. */
109
+ rank: (companion: string, terms: ReadonlyArray<string>) => SQL;
110
+ };
111
+ /**
112
+ * True when the engine ships SQLite's FTS5 module, which decides whether a
113
+ * search index is stored as an FTS5 shadow or as the portable inverted
114
+ * companion.
115
+ *
116
+ * A static property of the engine, so it is declared rather than probed:
117
+ * the previous `CREATE VIRTUAL TABLE` capability probe spent a round trip
118
+ * (and an error in the database's log) on every fresh connection to
119
+ * rediscover something the dialect already knows. Tests that want the
120
+ * portable layout override this instead of intercepting SQL strings.
121
+ */
122
+ supportsFts5: boolean;
70
123
  /** True when the engine supports `UPDATE/DELETE ... RETURNING` (SQLite/PG yes, MySQL no → use `affectedRows`). */
71
124
  supportsReturning: boolean;
72
125
  /**
73
- * The catalog probe for whether a physical `table` exists — backs the opt-in
74
- * companion-table (`__agg_`/`__rank_`) existence checks. Returns a drizzle
75
- * {@link SQL} (a non-empty result ⇒ the table exists) so it renders through
76
- * the same per-engine path as every other statement, never a hand-built
77
- * placeholder string. SQLite reads `sqlite_master`; Postgres/MySQL read
78
- * `information_schema.tables`.
79
- */
126
+ * The catalog probe for whether a physical `table` exists — backs the opt-in
127
+ * companion-table (`__agg_`/`__rank_`) existence checks. Returns a drizzle
128
+ * {@link SQL} (a non-empty result ⇒ the table exists) so it renders through
129
+ * the same per-engine path as every other statement, never a hand-built
130
+ * placeholder string. SQLite reads `sqlite_master`; Postgres/MySQL read
131
+ * `information_schema.tables`.
132
+ */
80
133
  tableExists: (table: string) => SQL;
134
+ /**
135
+ * Optional: btree operator class appended to an indexed text column so a
136
+ * `LIKE 'prefix%'` scan can use the index whatever the database collation.
137
+ * Postgres needs `text_pattern_ops` (a default `text_ops` btree built under
138
+ * e.g. `en_US.UTF-8` is useless to `LIKE`); SQLite and MySQL index prefix
139
+ * matches off the plain index and omit this. Only the search companion's
140
+ * token index reads it.
141
+ */
142
+ textPatternOperatorClass?: string;
81
143
  }
82
144
  export { SqlDialect, SqlExec, SqlRunResult };
package/dist/dialect.mjs CHANGED
@@ -1 +0,0 @@
1
-