@stacksjs/database 0.70.256 → 0.70.258

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.
@@ -2,6 +2,5 @@ import type { Ok } from '@stacksjs/error-handling';
2
2
  export declare function resetMysqlDatabase(): Promise<Ok<string, never>>;
3
3
  export declare function dropMysqlTables(): Promise<void>;
4
4
  export declare function generateMysqlMigration(modelPath: string): Promise<void>;
5
- export declare function generateMysqlTraitMigrations(): Promise<void>;
6
5
  export declare function createAlterTableMigration(modelPath: string): Promise<void>;
7
6
  export declare function generateIndexCreationSQL(tableName: string, index: { name: string, columns: string[], unique?: boolean, where?: string }): string;
@@ -3,7 +3,6 @@ function italic(str) {
3
3
  return `\x1B[3m${str}\x1B[23m`;
4
4
  }
5
5
  import { db } from "../utils";
6
- import { createPasswordResetsTable } from "./defaults/passwords";
7
6
  import { ok } from "@stacksjs/error-handling";
8
7
  import { fetchOtherModelRelations, getModelName, getPivotTables, getTableName } from "@stacksjs/orm";
9
8
  import { path } from "@stacksjs/path";
@@ -24,7 +23,7 @@ import {
24
23
  mapFieldTypeToColumnType,
25
24
  pluckChanges
26
25
  } from "./helpers";
27
- import { createCategorizableTable, createCommentablesTable, createCommentUpvoteMigration, createPasskeyMigration, createQueryLogsTable, createTaggablesTable, createTaggableTable, dropCommonTables } from "./defaults/traits";
26
+ import { dropCommonTables } from "./defaults/traits";
28
27
  export async function resetMysqlDatabase() {
29
28
  await dropMysqlTables();
30
29
  await deleteFrameworkModels();
@@ -70,18 +69,6 @@ export async function generateMysqlMigration(modelPath) {
70
69
  else
71
70
  await createTableMigration(modelPath);
72
71
  }
73
- export async function generateMysqlTraitMigrations() {
74
- await Promise.all([
75
- createCategorizableTable(),
76
- createCommentablesTable(),
77
- createTaggableTable(),
78
- createTaggablesTable(),
79
- createPasswordResetsTable(),
80
- createPasskeyMigration(),
81
- createQueryLogsTable(),
82
- createCommentUpvoteMigration()
83
- ]);
84
- }
85
72
  async function createTableMigration(modelPath) {
86
73
  log.debug("createTableMigration modelPath:", modelPath);
87
74
  const model = (await import(modelPath)).default, tableName = getTableName(model, modelPath), twoFactorEnabled = model.traits?.useAuth && typeof model.traits.useAuth !== "boolean" ? model.traits.useAuth.useTwoFactor : !1;
@@ -1,6 +1,5 @@
1
1
  import type { Ok } from '@stacksjs/error-handling';
2
2
  export declare function dropPostgresTables(): Promise<void>;
3
- export declare function generatePostgresTraitMigrations(): Promise<void>;
4
3
  export declare function resetPostgresDatabase(): Promise<Ok<string, never>>;
5
4
  export declare function generatePostgresMigration(modelPath: string): Promise<void>;
6
5
  export declare function generateIndexCreationSQL(tableName: string, index: { name: string, columns: string[], unique?: boolean, where?: string }): string;
@@ -3,7 +3,6 @@ function italic(str) {
3
3
  return `\x1B[3m${str}\x1B[23m`;
4
4
  }
5
5
  import { db } from "../utils";
6
- import { createPasswordResetsTable } from "./defaults/passwords";
7
6
  import { ok } from "@stacksjs/error-handling";
8
7
  import { fetchOtherModelRelations, getModelName, getPivotTables, getTableName } from "@stacksjs/orm";
9
8
  import { path } from "@stacksjs/path";
@@ -23,51 +22,19 @@ import {
23
22
  mapFieldTypeToColumnType,
24
23
  pluckChanges
25
24
  } from "./helpers";
26
- import {
27
- createPostgresCategorizableTable,
28
- createPostgresCommentablesPivotTable,
29
- createPostgresCommentsTable,
30
- createPostgresCommentUpvoteMigration,
31
- createPostgresPasskeyMigration,
32
- createPostgresQueryLogsTable,
33
- createPostgresTaggablesTable,
34
- createPostgresTagsTable,
35
- dropMigrationTables
36
- } from "./defaults/traits";
25
+ import { dropCommonTables, dropMigrationTables } from "./defaults/traits";
37
26
  export async function dropPostgresTables() {
38
27
  const tables = await fetchPostgresTables(), userModelFiles = globSync([path.userModelsPath("*.ts"), path.storagePath("framework/defaults/app/Models/**/*.ts")], { absolute: !0 });
39
28
  await dropMigrationTables();
40
29
  for (const table of tables)
41
30
  await db.unsafe(`DROP TABLE IF EXISTS "${table}" CASCADE`).execute();
42
- await dropCommonPostgresTables();
31
+ await dropCommonTables();
43
32
  for (const userModel of userModelFiles) {
44
33
  const userModelPath = (await import(userModel)).default, pivotTables = await getPivotTables(userModelPath, userModel);
45
34
  for (const pivotTable of pivotTables)
46
35
  await db.unsafe(`DROP TABLE IF EXISTS "${pivotTable.table}" CASCADE`).execute();
47
36
  }
48
37
  }
49
- async function dropCommonPostgresTables() {
50
- await db.unsafe('DROP TABLE IF EXISTS "passkeys" CASCADE').execute();
51
- await db.unsafe('DROP TABLE IF EXISTS "password_resets" CASCADE').execute();
52
- await db.unsafe('DROP TABLE IF EXISTS "query_logs" CASCADE').execute();
53
- await db.unsafe('DROP TABLE IF EXISTS "categorizables" CASCADE').execute();
54
- await db.unsafe('DROP TABLE IF EXISTS "commentables" CASCADE').execute();
55
- await db.unsafe('DROP TABLE IF EXISTS "comments" CASCADE').execute();
56
- await db.unsafe('DROP TABLE IF EXISTS "tags" CASCADE').execute();
57
- await db.unsafe('DROP TABLE IF EXISTS "taggables" CASCADE').execute();
58
- await db.unsafe('DROP TABLE IF EXISTS "commentable_upvotes" CASCADE').execute();
59
- }
60
- export async function generatePostgresTraitMigrations() {
61
- await createPostgresCategorizableTable();
62
- await createPostgresCommentsTable();
63
- await createPostgresTagsTable();
64
- await createPostgresCommentUpvoteMigration();
65
- await createPostgresPasskeyMigration();
66
- await createPostgresQueryLogsTable();
67
- await createPasswordResetsTable();
68
- await createPostgresCommentablesPivotTable();
69
- await createPostgresTaggablesTable();
70
- }
71
38
  export async function resetPostgresDatabase() {
72
39
  await dropPostgresTables();
73
40
  await deleteFrameworkModels();
package/dist/index.d.ts CHANGED
@@ -101,6 +101,26 @@ export * from './relation-columns';
101
101
  export { migrateNotificationTables } from './notification-tables';
102
102
  // RBAC tables migration (stacksjs/stacks#1941 Phase A)
103
103
  export { migrateRbacTables } from './rbac-tables';
104
+ // Polymorphic trait tables (commentables/taggables/categorizables/upvotes).
105
+ // Exported wholesale like auth-tables: the pure DDL builders are how tests and
106
+ // tooling stand up the same schema `buddy migrate` creates.
107
+ export * from './trait-tables';
108
+ // MySQL TIMESTAMP -> DATETIME guarantee for framework tables
109
+ export * from './datetime-columns';
110
+ // Dialect capability table — the single source of truth for what each
111
+ // dialect speaks (wire protocol) and what it accepts (DDL features).
112
+ export * from './dialect';
113
+ // Read replica routing policy (auto-route opt-in, transaction and
114
+ // read-your-writes carve-outs, replica selection).
115
+ export * from './replicas';
116
+ // DDL capability audit — catches a corpus that is valid SQL for the target's
117
+ // wire protocol but uses a feature the engine does not implement (foreign
118
+ // keys and AUTO_INCREMENT on a sharded engine).
119
+ export * from './ddl-constraints';
120
+ // VSchema derivation — turns the model relationship graph into a Vitess
121
+ // keyspace topology, co-locating child tables with their parents so joins
122
+ // between them do not scatter across shards.
123
+ export * from './vschema';
104
124
  // SQL dialect helpers & connection defaults
105
125
  export * from './sql-helpers';
106
126
  export * from './defaults';
package/dist/index.js CHANGED
@@ -27,6 +27,12 @@ export * from "./managed-columns";
27
27
  export * from "./relation-columns";
28
28
  export { migrateNotificationTables } from "./notification-tables";
29
29
  export { migrateRbacTables } from "./rbac-tables";
30
+ export * from "./trait-tables";
31
+ export * from "./datetime-columns";
32
+ export * from "./dialect";
33
+ export * from "./replicas";
34
+ export * from "./ddl-constraints";
35
+ export * from "./vschema";
30
36
  export * from "./sql-helpers";
31
37
  export * from "./defaults";
32
38
  export * from "./migration-dialect";
@@ -6,7 +6,7 @@ function getDbDriver() {
6
6
  return process.env.DB_CONNECTION || envVars.DB_CONNECTION || "sqlite";
7
7
  }
8
8
  export function notificationsTableSql(sql) {
9
- const { pkColumn, nullableTimestamp } = sql;
9
+ const { pkColumn, nullableTimestamp, datetime } = sql;
10
10
  return `CREATE TABLE IF NOT EXISTS notifications (
11
11
  ${pkColumn},
12
12
  user_id INTEGER NOT NULL,
@@ -14,25 +14,25 @@ export function notificationsTableSql(sql) {
14
14
  data TEXT NOT NULL,
15
15
  read_at ${nullableTimestamp},
16
16
  uuid VARCHAR(36),
17
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
17
+ created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
18
18
  updated_at ${nullableTimestamp}
19
19
  )`;
20
20
  }
21
21
  export function notificationPreferencesTableSql(sql) {
22
- const { pkColumn, boolTrue, nullableTimestamp } = sql;
22
+ const { pkColumn, boolTrue, nullableTimestamp, datetime } = sql;
23
23
  return `CREATE TABLE IF NOT EXISTS notification_preferences (
24
24
  ${pkColumn},
25
25
  user_id INTEGER NOT NULL,
26
26
  channel VARCHAR(50) NOT NULL,
27
27
  enabled BOOLEAN NOT NULL DEFAULT ${boolTrue},
28
28
  category VARCHAR(255),
29
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
29
+ created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
30
30
  updated_at ${nullableTimestamp},
31
31
  UNIQUE (user_id, channel, category)
32
32
  )`;
33
33
  }
34
34
  export function notificationDeliveriesTableSql(sql) {
35
- const { pkColumn, nullableTimestamp } = sql;
35
+ const { pkColumn, nullableTimestamp, datetime } = sql;
36
36
  return `CREATE TABLE IF NOT EXISTS notification_deliveries (
37
37
  ${pkColumn},
38
38
  user_id INTEGER,
@@ -44,7 +44,7 @@ export function notificationDeliveriesTableSql(sql) {
44
44
  error TEXT,
45
45
  metadata TEXT,
46
46
  sent_at ${nullableTimestamp},
47
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
47
+ created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
48
48
  updated_at ${nullableTimestamp}
49
49
  )`;
50
50
  }
@@ -1,3 +1,4 @@
1
+ import { sqlDateTime } from "./sql-helpers";
1
2
  import { memoryUsage } from "node:process";
2
3
  import { config } from "@stacksjs/config";
3
4
  import { log } from "@stacksjs/logging";
@@ -89,7 +90,7 @@ async function createQueryLogRecord(query, durationMs, status, error, bindings)
89
90
  connection,
90
91
  status,
91
92
  error: error ? String(error) : void 0,
92
- executed_at: new Date().toISOString(),
93
+ executed_at: sqlDateTime(),
93
94
  bindings,
94
95
  trace,
95
96
  ...caller,
@@ -4,11 +4,11 @@ export declare function rolesTableSql(sql: SqlHelpers): string;
4
4
  /** `permissions` table — same shape as `roles`. */
5
5
  export declare function permissionsTableSql(sql: SqlHelpers): string;
6
6
  /** `user_roles` pivot — composite PK makes double-assign a unique violation. */
7
- export declare function userRolesTableSql(): string;
7
+ export declare function userRolesTableSql(sql: SqlHelpers): string;
8
8
  /** `user_permissions` pivot. */
9
- export declare function userPermissionsTableSql(): string;
9
+ export declare function userPermissionsTableSql(sql: SqlHelpers): string;
10
10
  /** `role_permissions` pivot. */
11
- export declare function rolePermissionsTableSql(): string;
11
+ export declare function rolePermissionsTableSql(sql: SqlHelpers): string;
12
12
  /**
13
13
  * Create the 5 RBAC tables. Idempotent (`IF NOT EXISTS`), so it's
14
14
  * safe to run on every `buddy migrate`.
@@ -6,50 +6,53 @@ function getDbDriver() {
6
6
  return envVars.DB_CONNECTION || "sqlite";
7
7
  }
8
8
  export function rolesTableSql(sql) {
9
- const { pkColumn, nullableTimestamp } = sql;
9
+ const { pkColumn, nullableTimestamp, datetime } = sql;
10
10
  return `CREATE TABLE IF NOT EXISTS roles (
11
11
  ${pkColumn},
12
12
  name VARCHAR(255) NOT NULL,
13
13
  guard_name VARCHAR(255) NOT NULL DEFAULT 'web',
14
14
  description TEXT,
15
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
15
+ created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
16
16
  updated_at ${nullableTimestamp},
17
17
  UNIQUE (name, guard_name)
18
18
  )`;
19
19
  }
20
20
  export function permissionsTableSql(sql) {
21
- const { pkColumn, nullableTimestamp } = sql;
21
+ const { pkColumn, nullableTimestamp, datetime } = sql;
22
22
  return `CREATE TABLE IF NOT EXISTS permissions (
23
23
  ${pkColumn},
24
24
  name VARCHAR(255) NOT NULL,
25
25
  guard_name VARCHAR(255) NOT NULL DEFAULT 'web',
26
26
  description TEXT,
27
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
27
+ created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
28
28
  updated_at ${nullableTimestamp},
29
29
  UNIQUE (name, guard_name)
30
30
  )`;
31
31
  }
32
- export function userRolesTableSql() {
32
+ export function userRolesTableSql(sql) {
33
+ const { datetime } = sql;
33
34
  return `CREATE TABLE IF NOT EXISTS user_roles (
34
35
  user_id INTEGER NOT NULL,
35
36
  role_id INTEGER NOT NULL,
36
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
37
+ created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
37
38
  PRIMARY KEY (user_id, role_id)
38
39
  )`;
39
40
  }
40
- export function userPermissionsTableSql() {
41
+ export function userPermissionsTableSql(sql) {
42
+ const { datetime } = sql;
41
43
  return `CREATE TABLE IF NOT EXISTS user_permissions (
42
44
  user_id INTEGER NOT NULL,
43
45
  permission_id INTEGER NOT NULL,
44
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
46
+ created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
45
47
  PRIMARY KEY (user_id, permission_id)
46
48
  )`;
47
49
  }
48
- export function rolePermissionsTableSql() {
50
+ export function rolePermissionsTableSql(sql) {
51
+ const { datetime } = sql;
49
52
  return `CREATE TABLE IF NOT EXISTS role_permissions (
50
53
  role_id INTEGER NOT NULL,
51
54
  permission_id INTEGER NOT NULL,
52
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
55
+ created_at ${datetime} DEFAULT CURRENT_TIMESTAMP,
53
56
  PRIMARY KEY (role_id, permission_id)
54
57
  )`;
55
58
  }
@@ -66,13 +69,13 @@ export async function migrateRbacTables(options = {}) {
66
69
  await db.unsafe(permissionsTableSql(sql)).execute();
67
70
  if (options.verbose)
68
71
  log.info("Creating user_roles pivot...");
69
- await db.unsafe(userRolesTableSql()).execute();
72
+ await db.unsafe(userRolesTableSql(sql)).execute();
70
73
  if (options.verbose)
71
74
  log.info("Creating user_permissions pivot...");
72
- await db.unsafe(userPermissionsTableSql()).execute();
75
+ await db.unsafe(userPermissionsTableSql(sql)).execute();
73
76
  if (options.verbose)
74
77
  log.info("Creating role_permissions pivot...");
75
- await db.unsafe(rolePermissionsTableSql()).execute();
78
+ await db.unsafe(rolePermissionsTableSql(sql)).execute();
76
79
  if (options.verbose)
77
80
  log.success("RBAC tables created");
78
81
  return { success: !0 };
@@ -0,0 +1,52 @@
1
+ import type { ReadPolicyConfig, ReplicaConfig } from './driver-config';
2
+ /**
3
+ * Run `fn` in a fresh routing context.
4
+ *
5
+ * Called per request by the HTTP layer so read-your-writes is scoped to
6
+ * one request. Without an enclosing context the router falls back to
7
+ * treating every read as routable, which is the correct behavior for
8
+ * background work that has no request boundary to speak of — such work
9
+ * should use `db.read` explicitly if it wants a replica.
10
+ */
11
+ export declare function withRoutingContext<T>(fn: () => T): T;
12
+ /** Record that the current context has written, pinning its later reads. */
13
+ export declare function markContextWrote(): void;
14
+ /**
15
+ * Mark the current context as inside a transaction for the duration of
16
+ * `fn`, restoring the previous value afterwards so nested transactions
17
+ * unwind correctly.
18
+ */
19
+ export declare function withTransactionContext<T>(fn: () => Promise<T>): Promise<T>;
20
+ /** Whether the current async context has already issued a write. */
21
+ export declare function contextHasWritten(): boolean;
22
+ /** Whether the current async context is inside a transaction. */
23
+ export declare function contextInTransaction(): boolean;
24
+ /**
25
+ * Whether a read issued right now may go to a replica.
26
+ *
27
+ * The single decision point for rules 1-3. Exported so the routing
28
+ * behavior can be asserted directly in tests without standing up two
29
+ * database servers.
30
+ */
31
+ export declare function shouldRouteToReplica(options: {
32
+ policy?: ReadPolicyConfig
33
+ replicas?: ReplicaConfig[]
34
+ }): boolean;
35
+ /** Reset the cursor. Test-only — keeps selection assertions deterministic. */
36
+ export declare function resetReplicaCursor(): void;
37
+ /**
38
+ * Pick a replica according to the configured strategy.
39
+ *
40
+ * Returns `undefined` for an empty list so callers fall back to the
41
+ * primary rather than having to pre-check.
42
+ */
43
+ export declare function selectReplica(replicas: ReplicaConfig[], strategy?: ReadPolicyConfig['strategy']): ReplicaConfig | undefined;
44
+ /**
45
+ * Resolve a replica's connection settings against its primary.
46
+ *
47
+ * A replica declares only what differs — usually just the host — so
48
+ * everything else is inherited. Getting this wrong in the other direction
49
+ * (requiring full credentials per replica) is how host lists drift out of
50
+ * sync with a rotated password.
51
+ */
52
+ export declare function resolveReplicaConnection(replica: ReplicaConfig, primary: { name?: string, database?: string, host?: string, port?: number, username?: string, password?: string }): { database: string, host: string, port?: number, username?: string, password?: string };
@@ -0,0 +1,74 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const routingContext = new AsyncLocalStorage;
3
+ export function withRoutingContext(fn) {
4
+ return routingContext.run({ wroteInContext: !1, inTransaction: !1 }, fn);
5
+ }
6
+ export function markContextWrote() {
7
+ const store = routingContext.getStore();
8
+ if (store)
9
+ store.wroteInContext = !0;
10
+ }
11
+ export async function withTransactionContext(fn) {
12
+ const store = routingContext.getStore();
13
+ if (!store)
14
+ return fn();
15
+ const previous = store.inTransaction;
16
+ store.inTransaction = !0;
17
+ try {
18
+ return await fn();
19
+ } finally {
20
+ store.inTransaction = previous;
21
+ }
22
+ }
23
+ export function contextHasWritten() {
24
+ return routingContext.getStore()?.wroteInContext ?? !1;
25
+ }
26
+ export function contextInTransaction() {
27
+ return routingContext.getStore()?.inTransaction ?? !1;
28
+ }
29
+ export function shouldRouteToReplica(options) {
30
+ const { policy, replicas } = options;
31
+ if (!replicas?.length)
32
+ return !1;
33
+ if (!policy?.autoRoute)
34
+ return !1;
35
+ if (contextInTransaction())
36
+ return !1;
37
+ if (contextHasWritten())
38
+ return !1;
39
+ return !0;
40
+ }
41
+ let roundRobinCursor = 0;
42
+ export function resetReplicaCursor() {
43
+ roundRobinCursor = 0;
44
+ }
45
+ export function selectReplica(replicas, strategy = "round-robin", random = Math.random) {
46
+ if (!replicas.length)
47
+ return;
48
+ if (replicas.length === 1)
49
+ return replicas[0];
50
+ if (strategy === "random")
51
+ return replicas[Math.floor(random() * replicas.length)];
52
+ if (strategy === "weighted") {
53
+ const weights = replicas.map((r) => Math.max(0, r.weight ?? 1)), total = weights.reduce((sum, w) => sum + w, 0);
54
+ if (total <= 0)
55
+ return replicas[roundRobinCursor++ % replicas.length];
56
+ let ticket = random() * total;
57
+ for (let i = 0;i < replicas.length; i++) {
58
+ ticket -= weights[i];
59
+ if (ticket < 0)
60
+ return replicas[i];
61
+ }
62
+ return replicas[replicas.length - 1];
63
+ }
64
+ return replicas[roundRobinCursor++ % replicas.length];
65
+ }
66
+ export function resolveReplicaConnection(replica, primary) {
67
+ return {
68
+ database: primary.name ?? primary.database ?? "",
69
+ host: replica.host,
70
+ port: replica.port ?? primary.port,
71
+ username: replica.username ?? primary.username,
72
+ password: replica.password ?? primary.password
73
+ };
74
+ }
package/dist/schema.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { Table } from './table';
1
2
  /**
2
3
  * @defaultValue
3
4
  * ```ts
@@ -1,3 +1,62 @@
1
+ /**
2
+ * The framework's canonical datetime literal: ISO-8601 UTC **without** the
3
+ * trailing `Z` (`2026-08-04T01:52:47.417`).
4
+ *
5
+ * One format for every dialect, because the alternatives are all broken:
6
+ *
7
+ * - `new Date().toISOString()` — what the framework used to write — is
8
+ * rejected outright by MySQL in strict mode ("Incorrect datetime value"),
9
+ * so any insert into a TIMESTAMP column threw. Dropping the `Z` is the
10
+ * whole difference; MySQL accepts the rest of the ISO shape.
11
+ * - A space-separated literal (`2026-08-04 01:52:47`) is accepted everywhere
12
+ * but sorts BEFORE an ISO string on SQLite, where these columns hold text.
13
+ * Mixing it with already-stored ISO rows would corrupt every ordering and
14
+ * range query over existing data.
15
+ *
16
+ * Keeping the `T` and dropping only the `Z` satisfies all three engines and
17
+ * still sorts correctly against rows written in the old format, since the
18
+ * value is a prefix of the old one for the same instant.
19
+ *
20
+ * Use this for every app-generated timestamp written to, or compared against,
21
+ * a framework table — never the raw `toISOString()`, and never the database's
22
+ * own clock (see below).
23
+ *
24
+ * ## Do not compare these against `NOW()` / `datetime('now')`
25
+ *
26
+ * The DB clock renders as `2026-08-04 01:52:47` — space-separated. On SQLite
27
+ * these columns are text, and `'T' > ' '`, so an ISO value always compares
28
+ * greater than a same-day DB-clock value regardless of the actual instant.
29
+ * That silently made every `expires_at > datetime('now')` check pass for
30
+ * already-expired rows. Compare app-written columns against `sqlDateTime()`.
31
+ */
32
+ export declare function sqlDateTime(value?: Date): string;
33
+ /**
34
+ * {@link sqlDateTime} pre-quoted for interpolation into a raw SQL string.
35
+ * The value is generated from a `Date`, never from user input, so it cannot
36
+ * carry a quote to escape.
37
+ */
38
+ export declare function sqlDateTimeLiteral(value?: Date): string;
39
+ /**
40
+ * Read a timestamp back out of a framework table.
41
+ *
42
+ * The counterpart to {@link sqlDateTime}, and mandatory wherever a stored
43
+ * timestamp is compared in JavaScript. `new Date('2026-08-04T01:52:47.417')`
44
+ * — an ISO date-time with no offset — is parsed as **local** time per the ES
45
+ * spec, while the same string with a `Z` is UTC. Since the stored format
46
+ * cannot carry the `Z` (MySQL rejects it), a bare `new Date(...)` on these
47
+ * values silently shifts every comparison by the host's UTC offset. Only a
48
+ * server already running in UTC would look correct.
49
+ *
50
+ * Accepts every shape these columns hold: the canonical `T` format, the
51
+ * space-separated form the database clocks emit, values that still carry a
52
+ * `Z` or an explicit offset from before this format existed, and the `Date`
53
+ * objects the MySQL driver hands back. Anything without an explicit offset is
54
+ * read as UTC, which is what the framework writes.
55
+ *
56
+ * Returns null for missing or unparseable input so callers can fail closed
57
+ * rather than treating a bad value as the epoch.
58
+ */
59
+ export declare function parseSqlDateTime(value: unknown): Date | null;
1
60
  /**
2
61
  * Create SQL dialect helpers for a given driver.
3
62
  *
@@ -9,13 +68,6 @@
9
68
  * ```
10
69
  */
11
70
  export declare function sqlHelpers(driver: string): SqlDialectHelpers;
12
- /**
13
- * SQL Dialect Helpers
14
- *
15
- * Cross-database compatibility utilities for PostgreSQL, MySQL, and SQLite.
16
- * Centralizes the isPostgres/isMysql/now/boolTrue/boolFalse/param helpers
17
- * that were previously duplicated across tokens.ts, auth-tables.ts, and setup.ts.
18
- */
19
71
  export declare interface SqlDialectHelpers {
20
72
  driver: string
21
73
  isPostgres: boolean
@@ -27,6 +79,7 @@ export declare interface SqlDialectHelpers {
27
79
  autoIncrement: string
28
80
  primaryKey: string
29
81
  pkColumn: string
82
+ datetime: string
30
83
  nullableTimestamp: string
31
84
  param: (index: number) => string
32
85
  params: (...values: unknown[]) => { sql: string, values: unknown[] }
@@ -1,17 +1,43 @@
1
+ import { dialectCapabilities } from "./dialect";
2
+ export function sqlDateTime(value = new Date) {
3
+ return value.toISOString().slice(0, -1);
4
+ }
5
+ export function sqlDateTimeLiteral(value = new Date) {
6
+ return `'${sqlDateTime(value)}'`;
7
+ }
8
+ export function parseSqlDateTime(value) {
9
+ if (value === null || value === void 0)
10
+ return null;
11
+ if (value instanceof Date)
12
+ return Number.isNaN(value.getTime()) ? null : value;
13
+ if (typeof value === "number")
14
+ return Number.isNaN(value) ? null : new Date(value);
15
+ if (typeof value !== "string")
16
+ return null;
17
+ const trimmed = value.trim();
18
+ if (!trimmed)
19
+ return null;
20
+ let normalized = trimmed.replace(" ", "T");
21
+ if (!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(normalized))
22
+ normalized += "Z";
23
+ const parsed = new Date(normalized);
24
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
25
+ }
1
26
  export function sqlHelpers(driver) {
2
- const isPostgres = driver === "postgres", isMysql = driver === "mysql" || driver === "singlestore";
27
+ const caps = dialectCapabilities(driver), isPostgres = caps.wire === "postgres", isMysql = caps.wire === "mysql", isSqlite = caps.wire === "sqlite";
3
28
  return {
4
29
  driver,
5
30
  isPostgres,
6
31
  isMysql,
7
- isSqlite: !isPostgres && !isMysql,
32
+ isSqlite,
8
33
  now: isPostgres || isMysql ? "NOW()" : "datetime('now')",
9
34
  boolTrue: isPostgres ? "true" : "1",
10
35
  boolFalse: isPostgres ? "false" : "0",
11
36
  autoIncrement: isPostgres ? "SERIAL" : "INTEGER",
12
- primaryKey: isPostgres ? "PRIMARY KEY" : isMysql ? "PRIMARY KEY AUTO_INCREMENT" : "PRIMARY KEY AUTOINCREMENT",
13
- pkColumn: isPostgres ? "id SERIAL PRIMARY KEY" : isMysql ? "id INTEGER PRIMARY KEY AUTO_INCREMENT" : "id INTEGER PRIMARY KEY AUTOINCREMENT",
14
- nullableTimestamp: isMysql ? "TIMESTAMP NULL" : "TIMESTAMP",
37
+ primaryKey: !caps.supportsAutoIncrement ? "PRIMARY KEY" : isPostgres ? "PRIMARY KEY" : isMysql ? "PRIMARY KEY AUTO_INCREMENT" : "PRIMARY KEY AUTOINCREMENT",
38
+ pkColumn: !caps.supportsAutoIncrement ? "id BIGINT NOT NULL PRIMARY KEY" : isPostgres ? "id SERIAL PRIMARY KEY" : isMysql ? "id INTEGER PRIMARY KEY AUTO_INCREMENT" : "id INTEGER PRIMARY KEY AUTOINCREMENT",
39
+ datetime: isMysql ? "DATETIME" : "TIMESTAMP",
40
+ nullableTimestamp: isMysql ? "DATETIME NULL" : "TIMESTAMP",
15
41
  param(index) {
16
42
  return isPostgres ? `$${index}` : "?";
17
43
  },