@stacksjs/database 0.70.88 → 0.70.91

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.
Files changed (83) hide show
  1. package/dist/auth-tables.d.ts +60 -0
  2. package/dist/auth-tables.js +220 -0
  3. package/dist/class-seeder.d.ts +65 -0
  4. package/dist/class-seeder.js +116 -0
  5. package/dist/column.d.ts +17 -0
  6. package/dist/column.js +26 -0
  7. package/dist/custom/audits.d.ts +16 -0
  8. package/dist/custom/audits.js +57 -0
  9. package/dist/custom/errors.d.ts +1 -0
  10. package/dist/custom/errors.js +48 -0
  11. package/dist/custom/index.d.ts +3 -0
  12. package/dist/custom/index.js +3 -0
  13. package/dist/custom/jobs.d.ts +3 -0
  14. package/dist/custom/jobs.js +449 -0
  15. package/dist/database.d.ts +89 -0
  16. package/dist/database.js +178 -0
  17. package/dist/defaults.d.ts +48 -0
  18. package/dist/defaults.js +48 -0
  19. package/dist/driver-config.d.ts +149 -0
  20. package/dist/driver-config.js +144 -0
  21. package/dist/drivers/defaults/index.d.ts +2 -0
  22. package/dist/drivers/defaults/index.js +2 -0
  23. package/dist/drivers/defaults/passwords.d.ts +4 -0
  24. package/dist/drivers/defaults/passwords.js +106 -0
  25. package/dist/drivers/defaults/traits.d.ts +33 -0
  26. package/dist/drivers/defaults/traits.js +1125 -0
  27. package/dist/drivers/dynamodb.d.ts +200 -0
  28. package/dist/drivers/dynamodb.js +607 -0
  29. package/dist/drivers/helpers.d.ts +35 -0
  30. package/dist/drivers/helpers.js +206 -0
  31. package/dist/drivers/index.d.ts +16 -0
  32. package/dist/drivers/index.js +9 -0
  33. package/dist/drivers/mysql.d.ts +7 -0
  34. package/dist/drivers/mysql.js +322 -0
  35. package/dist/drivers/postgres.d.ts +7 -0
  36. package/dist/drivers/postgres.js +411 -0
  37. package/dist/drivers/sqlite.d.ts +20 -0
  38. package/dist/drivers/sqlite.js +397 -0
  39. package/dist/factory.d.ts +41 -0
  40. package/dist/factory.js +51 -0
  41. package/dist/fk-audit.d.ts +101 -0
  42. package/dist/fk-audit.js +181 -0
  43. package/dist/index.d.ts +149 -0
  44. package/dist/index.js +55 -0
  45. package/dist/migration-lock.d.ts +23 -0
  46. package/dist/migration-lock.js +143 -0
  47. package/dist/migrations.d.ts +76 -0
  48. package/dist/migrations.js +549 -0
  49. package/dist/notification-tables.d.ts +20 -0
  50. package/dist/notification-tables.js +54 -0
  51. package/dist/query-logger.d.ts +26 -0
  52. package/dist/query-logger.js +213 -0
  53. package/dist/query-parser.d.ts +4 -0
  54. package/dist/query-parser.js +93 -0
  55. package/dist/rbac-tables.d.ts +17 -0
  56. package/dist/rbac-tables.js +84 -0
  57. package/dist/safe-migrations.d.ts +72 -0
  58. package/dist/safe-migrations.js +59 -0
  59. package/dist/schema.d.ts +4 -0
  60. package/dist/schema.js +10 -0
  61. package/dist/seed-scaffold.d.ts +34 -0
  62. package/dist/seed-scaffold.js +144 -0
  63. package/dist/seeder.d.ts +116 -0
  64. package/dist/seeder.js +363 -0
  65. package/dist/sql-helpers.d.ts +33 -0
  66. package/dist/sql-helpers.js +24 -0
  67. package/dist/table.d.ts +7 -0
  68. package/dist/table.js +26 -0
  69. package/dist/tools/setup.d.ts +1 -0
  70. package/dist/tools/setup.js +6 -0
  71. package/dist/transaction-context.d.ts +52 -0
  72. package/dist/transaction-context.js +62 -0
  73. package/dist/types.d.ts +151 -0
  74. package/dist/types.js +23 -0
  75. package/dist/unique-audit.d.ts +60 -0
  76. package/dist/unique-audit.js +174 -0
  77. package/dist/utils.d.ts +189 -0
  78. package/dist/utils.js +163 -0
  79. package/dist/uuid-columns.d.ts +22 -0
  80. package/dist/uuid-columns.js +68 -0
  81. package/dist/validators.d.ts +26 -0
  82. package/dist/validators.js +122 -0
  83. package/package.json +11 -11
package/dist/table.js ADDED
@@ -0,0 +1,26 @@
1
+ import { log } from "@stacksjs/logging";
2
+ import { Column } from "./column";
3
+
4
+ export class Table {
5
+ columns = [];
6
+ increments(name) {
7
+ const column = new Column(name, "integer", {
8
+ primaryKey: !0,
9
+ autoIncrement: !0
10
+ });
11
+ this.columns.push(column);
12
+ return column;
13
+ }
14
+ string(name, varchar = 255) {
15
+ const column = new Column(name, `varchar(${varchar})`);
16
+ this.columns.push(column);
17
+ return column;
18
+ }
19
+ timestamps() {
20
+ this.columns.push(new Column("created_at", "timestamp"));
21
+ this.columns.push(new Column("updated_at", "timestamp"));
22
+ }
23
+ execute() {
24
+ log.info(`Creating table with columns: ${this.columns.map((col) => col.name).join(", ")}`);
25
+ }
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import { dynamoDb } from "dynamodb-tooling";
2
+ const port = 8000;
3
+ dynamoDb.launch({
4
+ port,
5
+ additionalArgs: ["-sharedDb"]
6
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * True if the current async context is inside an active transaction
3
+ * scope. Queue dispatch (and other side-effect emitters) read this
4
+ * to decide between immediate execution and buffering.
5
+ */
6
+ export declare function isInTransaction(): boolean;
7
+ /**
8
+ * Enqueue a callback to fire after the surrounding transaction
9
+ * commits. Returns:
10
+ * - `true` — buffered; caller should NOT execute the side-effect now
11
+ * - `false` — no active transaction; caller should execute immediately
12
+ *
13
+ * This is the low-level primitive. Higher-level facades (queue
14
+ * dispatch, mailer send, event emit) wrap it with their own
15
+ * "respect transaction context unless overridden" logic.
16
+ */
17
+ export declare function enqueueAfterCommit(callback: AfterCommitCallback): boolean;
18
+ /**
19
+ * Run `fn` inside a transaction scope. Returns whatever `fn`
20
+ * returns. On success, fires every buffered after-commit callback
21
+ * in insertion order. On error, discards them — the transaction
22
+ * rolled back so the side-effects shouldn't happen.
23
+ *
24
+ * Used by `@stacksjs/orm`'s `transaction()` wrapper to thread the
25
+ * scope through user code. Apps don't call this directly.
26
+ *
27
+ * Nested calls reuse the outer scope rather than nesting — flush
28
+ * happens on the OUTERMOST commit, matching the savepoint
29
+ * semantics of every relational database. The depth counter is
30
+ * tracked so the outer call knows when it owns the flush.
31
+ */
32
+ export declare function runInTransactionScope<T>(fn: () => Promise<T>, options?: { onError?: (err: unknown, index: number) => void }): Promise<T>;
33
+ /**
34
+ * Test-only escape hatch — manually flush the current scope's
35
+ * buffered callbacks without ending the transaction. Production
36
+ * code never needs this; tests use it to assert intermediate
37
+ * state. Returns the number of callbacks fired.
38
+ */
39
+ export declare function __flushAfterCommitNow(): Promise<number>;
40
+ /**
41
+ * Test-only escape hatch — peek at the number of buffered
42
+ * callbacks without firing them. Returns 0 outside a scope.
43
+ */
44
+ export declare function __pendingAfterCommitCount(): number;
45
+ /**
46
+ * One buffered side-effect waiting for the surrounding transaction
47
+ * to commit. Errors thrown during flush are NOT re-thrown — the
48
+ * transaction itself already committed, so failing the whole flow
49
+ * after-the-fact would corrupt the caller's mental model. Errors
50
+ * are logged via `onError` if the scope supplied one.
51
+ */
52
+ declare type AfterCommitCallback = () => Promise<void> | void;
@@ -0,0 +1,62 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ const transactionStorage = new AsyncLocalStorage;
3
+ export function isInTransaction() {
4
+ return transactionStorage.getStore() !== void 0;
5
+ }
6
+ export function enqueueAfterCommit(callback) {
7
+ const scope = transactionStorage.getStore();
8
+ if (!scope)
9
+ return !1;
10
+ scope.pending.push(callback);
11
+ return !0;
12
+ }
13
+ export async function runInTransactionScope(fn, options = {}) {
14
+ const existing = transactionStorage.getStore();
15
+ if (existing) {
16
+ existing.depth += 1;
17
+ try {
18
+ return await fn();
19
+ } finally {
20
+ existing.depth -= 1;
21
+ }
22
+ }
23
+ const scope = {
24
+ pending: [],
25
+ depth: 1,
26
+ onError: options.onError
27
+ };
28
+ let result;
29
+ try {
30
+ result = await transactionStorage.run(scope, fn);
31
+ } catch (err) {
32
+ scope.pending.length = 0;
33
+ throw err;
34
+ }
35
+ await flushScope(scope);
36
+ return result;
37
+ }
38
+ async function flushScope(scope) {
39
+ for (let i = 0;i < scope.pending.length; i++)
40
+ try {
41
+ await scope.pending[i]();
42
+ } catch (err) {
43
+ if (scope.onError)
44
+ try {
45
+ scope.onError(err, i);
46
+ } catch {}
47
+ else
48
+ console.error("[transaction-context] after-commit callback threw:", err);
49
+ }
50
+ scope.pending.length = 0;
51
+ }
52
+ export async function __flushAfterCommitNow() {
53
+ const scope = transactionStorage.getStore();
54
+ if (!scope)
55
+ return 0;
56
+ const count = scope.pending.length;
57
+ await flushScope(scope);
58
+ return count;
59
+ }
60
+ export function __pendingAfterCommitCount() {
61
+ return transactionStorage.getStore()?.pending.length ?? 0;
62
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * SQL template tag function.
3
+ * Creates parameterized SQL queries from template literals.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const query = sql`SELECT * FROM users WHERE id = ${userId}`
8
+ * ```
9
+ */
10
+ export declare function sql(strings: TemplateStringsArray, ...values: unknown[]): Sql;
11
+ /**
12
+ * Type for raw SQL expressions.
13
+ * Used when building dynamic SQL queries.
14
+ */
15
+ export declare interface RawBuilder<T = unknown> {
16
+ readonly sql: string
17
+ readonly parameters?: unknown[]
18
+ readonly __result?: T
19
+ }
20
+ /**
21
+ * SQL template tag type.
22
+ * Used for tagged template literals that produce SQL.
23
+ */
24
+ export declare interface Sql {
25
+ readonly sql: string
26
+ readonly parameters: unknown[]
27
+ }
28
+ /**
29
+ * Reference to a column for use inside an expression. Returned by
30
+ * {@link StacksExpressionBuilder.ref} and accepted everywhere a value
31
+ * or column is expected (e.g. inside `sql\`\${ref} > 0\`\`).
32
+ *
33
+ * The shape matches `sql.ref()`'s return so a raw fragment from either
34
+ * source is interoperable.
35
+ */
36
+ export declare interface ColumnRef {
37
+ readonly raw: string
38
+ as: (alias: string) => ColumnRef
39
+ }
40
+ /**
41
+ * Fluent aggregate-function builder accessible via
42
+ * `eb.fn.count(...)`, `eb.fn.sum(...)`, etc. The chained `.as(name)`
43
+ * names the resulting column in the projection; `.filterWhere(...)`
44
+ * scopes the aggregate to a sub-population (`COUNT(*) FILTER (WHERE
45
+ * status = 'success')` style).
46
+ */
47
+ export declare interface AggregateExpression {
48
+ as: (alias: string) => AggregateExpression
49
+ filterWhere: (column: string, op: ExpressionOperator | string, value: unknown) => AggregateExpression
50
+ }
51
+ /**
52
+ * Aggregate-function accessor exposed on the expression builder.
53
+ *
54
+ * Covers the call sites in commerce today (`count`, `sum`, `avg`,
55
+ * `min`, `max`). Other Kysely-side aggregates (`countAll`,
56
+ * `coalesce`, etc.) can be added here as call sites surface; we
57
+ * deliberately don't widen to "everything Kysely exposes" because
58
+ * that surface keeps growing and an `any`-typed escape hatch always
59
+ * exists (`eb.fn as any).newThing(...)`) if a one-off bypass is
60
+ * genuinely needed.
61
+ */
62
+ export declare interface ExpressionFunctions {
63
+ countAll: () => AggregateExpression
64
+ count: (column: string) => AggregateExpression
65
+ sum: (column: string) => AggregateExpression
66
+ avg: (column: string) => AggregateExpression
67
+ min: (column: string) => AggregateExpression
68
+ max: (column: string) => AggregateExpression
69
+ }
70
+ /**
71
+ * Minimal typed expression-builder surface for sub-query / inline-
72
+ * expression callbacks (stacksjs/stacks#1892, T-2 from #1875).
73
+ *
74
+ * Background: the framework's commerce module passed `(eb: any) => …`
75
+ * to `.where()` / `.select()` callbacks across 80+ sites. The `any`
76
+ * escape meant typos like `eb.compare(...)` (no such method — it's
77
+ * `cmpr`) only surfaced at runtime, and any later rename in
78
+ * bun-query-builder couldn't break here at type-check time.
79
+ *
80
+ * This interface declares the methods commerce actually uses today —
81
+ * `or`, `cmpr`, `ref`, `raw`, plus the `fn` aggregate accessor. It
82
+ * intentionally does NOT claim to be the full Kysely
83
+ * `ExpressionBuilder<DB, TB>` type:
84
+ *
85
+ * - Stacks's `Database` is still typed as `any` (no generated
86
+ * schema map yet) so the table-aware narrowing Kysely offers
87
+ * can't be expressed here yet.
88
+ * - bun-query-builder doesn't currently re-export its internal
89
+ * `ExpressionBuilder` type, so we can't alias to the canonical
90
+ * shape upstream.
91
+ *
92
+ * When either of those changes upstream, swap this interface's
93
+ * implementation in one place rather than re-typing every call site.
94
+ */
95
+ export declare interface StacksExpressionBuilder {
96
+ (left: unknown, op: ExpressionOperator | string, right: unknown): unknown
97
+ or: (expressions: ReadonlyArray<unknown>) => unknown
98
+ and?: (expressions: ReadonlyArray<unknown>) => unknown
99
+ cmpr: (left: unknown, op: ExpressionOperator, right: unknown) => unknown
100
+ ref: (column: string) => ColumnRef
101
+ raw: (value: string) => ColumnRef
102
+ fn: ExpressionFunctions
103
+ readonly [extra: string]: unknown
104
+ }
105
+ /**
106
+ * Database types - Compatibility layer
107
+ *
108
+ * These types provide backwards compatibility with code that
109
+ * previously used Kysely types. They work with bun-query-builder's
110
+ * native type system.
111
+ */
112
+ /**
113
+ * Marks a column as auto-generated (e.g., auto-increment primary keys).
114
+ * When inserting, this field is optional. When selecting, it's required.
115
+ */
116
+ export type Generated<T> = T;
117
+ /**
118
+ * Marks a column as always generated (computed columns).
119
+ * This field cannot be inserted or updated directly.
120
+ */
121
+ export type GeneratedAlways<T> = T;
122
+ /**
123
+ * Utility type for insert operations.
124
+ * Makes Generated fields optional, keeps required fields required.
125
+ */
126
+ export type Insertable<T> = {
127
+ [K in keyof T]?: T[K]
128
+ }
129
+ /**
130
+ * Utility type for select operations.
131
+ * All fields are as defined in the table type.
132
+ */
133
+ export type Selectable<T> = T;
134
+ /**
135
+ * Utility type for update operations.
136
+ * All fields are optional since you may update only some fields.
137
+ */
138
+ export type Updateable<T> = Partial<T>;
139
+ /**
140
+ * Database type alias for backwards compatibility.
141
+ * Use the query builder from bun-query-builder instead.
142
+ */
143
+ export type Database = any;
144
+ /**
145
+ * Comparison operator accepted by {@link StacksExpressionBuilder.cmpr}
146
+ * and friends. Mirrors the standard SQL operators the underlying
147
+ * Kysely-style builder supports.
148
+ */
149
+ export type ExpressionOperator = | '=' | '!=' | '<>' | '<' | '<=' | '>' | '>='
150
+ | 'in' | 'not in' | 'is' | 'is not'
151
+ | 'like' | 'not like' | 'ilike' | 'not ilike';
package/dist/types.js ADDED
@@ -0,0 +1,23 @@
1
+ export function sql(strings, ...values) {
2
+ const sqlParts = [], parameters = [];
3
+ for (let i = 0;i < strings.length; i++) {
4
+ sqlParts.push(strings[i]);
5
+ if (i < values.length)
6
+ if (values[i] && typeof values[i] === "object" && "raw" in values[i])
7
+ sqlParts.push(values[i].raw);
8
+ else {
9
+ sqlParts.push("?");
10
+ parameters.push(values[i]);
11
+ }
12
+ }
13
+ return {
14
+ sql: sqlParts.join(""),
15
+ parameters
16
+ };
17
+ }
18
+ sql.raw = function raw(value) {
19
+ return { raw: value };
20
+ };
21
+ sql.ref = function ref(column) {
22
+ return { raw: column };
23
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Walk every model file (user + framework defaults) and return the
3
+ * full list of declared unique constraints — both single-column
4
+ * `unique: true` attributes and `unique: true` composite indexes.
5
+ */
6
+ export declare function getDeclaredUniques(): Promise<DeclaredUnique[]>;
7
+ /**
8
+ * Query the live database for every UNIQUE index, returning a
9
+ * normalised `{ table, name, columns }` shape. Dialect-aware: PRAGMA
10
+ * on SQLite, information_schema / pg_index on MySQL / PostgreSQL.
11
+ *
12
+ * The optional `dialect` parameter is the test seam — production code
13
+ * leaves it undefined so it's derived from `DB_CONNECTION`.
14
+ */
15
+ export declare function getLiveUniqueIndexes(dialect?: Dialect): Promise<LiveUniqueIndex[]>;
16
+ /**
17
+ * Diff declared unique constraints against live UNIQUE indexes. A
18
+ * declared entry is satisfied iff some live unique index on the same
19
+ * table has an equal column SET (sorted, case-insensitive). Declared
20
+ * entries whose table is absent from the live DB go to `skippedTables`
21
+ * (feature-gated commerce/CMS models legitimately have no table yet)
22
+ * rather than `missing`.
23
+ */
24
+ export declare function auditUniqueIndexes(dialect?: Dialect): Promise<UniqueAuditResult>;
25
+ // stacksjs/stacks#1952 — Unique-index drift audit. Compares each
26
+ // model's declared uniqueness (`unique: true` attributes and
27
+ // `unique: true` composite indexes) against the UNIQUE indexes that
28
+ // actually exist in the live database.
29
+ //
30
+ // This is the `buddy doctor` companion to the migrate-side self-heal
31
+ // (#1952): `buddy migrate` already re-queues missing unique-index
32
+ // migrations, but that only fires when you run migrate, doesn't help
33
+ // apps scaffolded during the stub era (whose own repos carry
34
+ // `SELECT 1;` stub migrations), and hard-fails mid-migrate on
35
+ // pre-existing duplicate rows instead of reporting first. This audit
36
+ // surfaces the drift read-only, before you migrate.
37
+ //
38
+ // Matching is by COLUMN SET, never by index name — the generator
39
+ // emits names like `users_users_email_unique` while migration
40
+ // filenames say `users_email_unique`, so names are not stable.
41
+ export declare interface DeclaredUnique {
42
+ table: string
43
+ columns: string[]
44
+ model: string
45
+ source: 'attribute' | 'index'
46
+ indexName?: string
47
+ }
48
+ export declare interface LiveUniqueIndex {
49
+ table: string
50
+ name: string
51
+ columns: string[]
52
+ }
53
+ export declare interface UniqueAuditResult {
54
+ supported: boolean
55
+ declared: DeclaredUnique[]
56
+ live: LiveUniqueIndex[]
57
+ missing: DeclaredUnique[]
58
+ skippedTables: string[]
59
+ }
60
+ declare type Dialect = 'sqlite' | 'mysql' | 'postgres' | 'other';
@@ -0,0 +1,174 @@
1
+ import { path } from "@stacksjs/path";
2
+ import { plural, snakeCase } from "@stacksjs/strings";
3
+ import { safeGlob } from "./fk-audit";
4
+ export async function getDeclaredUniques() {
5
+ const modelFiles = [
6
+ ...safeGlob(path.userModelsPath("*.ts")),
7
+ ...safeGlob(path.storagePath("framework/defaults/app/Models/**/*.ts"))
8
+ ], declared = [];
9
+ for (const modelFile of modelFiles) {
10
+ let model;
11
+ try {
12
+ model = (await import(modelFile)).default;
13
+ } catch {
14
+ continue;
15
+ }
16
+ if (!model || typeof model !== "object")
17
+ continue;
18
+ const table = model.table || plural(snakeCase(model.name || "")), modelName = model.name || "", attributes = model.attributes;
19
+ if (attributes && typeof attributes === "object") {
20
+ for (const [field, attr] of Object.entries(attributes))
21
+ if (attr && typeof attr === "object" && attr.unique === !0)
22
+ declared.push({
23
+ table,
24
+ columns: [snakeCase(field)],
25
+ model: modelName,
26
+ source: "attribute"
27
+ });
28
+ }
29
+ const indexes = model.indexes;
30
+ if (Array.isArray(indexes)) {
31
+ for (const index of indexes)
32
+ if (index && index.unique === !0 && Array.isArray(index.columns) && index.columns.length > 0)
33
+ declared.push({
34
+ table,
35
+ columns: index.columns.map((c) => snakeCase(c)),
36
+ model: modelName,
37
+ source: "index",
38
+ indexName: index.name
39
+ });
40
+ }
41
+ }
42
+ return declared;
43
+ }
44
+ export async function getLiveUniqueIndexes(dialect) {
45
+ const { db } = await import("./utils"), d = dialect ?? await currentDialect();
46
+ if (d === "sqlite")
47
+ return getSqliteLiveUniques(db);
48
+ if (d === "mysql")
49
+ return getMysqlLiveUniques(db);
50
+ if (d === "postgres")
51
+ return getPostgresLiveUniques(db);
52
+ return [];
53
+ }
54
+ export async function auditUniqueIndexes(dialect) {
55
+ const d = dialect ?? await currentDialect();
56
+ if (d !== "sqlite" && d !== "mysql" && d !== "postgres")
57
+ return { supported: !1, declared: [], live: [], missing: [], skippedTables: [] };
58
+ const declared = await getDeclaredUniques(), live = await getLiveUniqueIndexes(d), liveTables = await getLiveTables(d), liveByTable = new Map;
59
+ for (const idx of live) {
60
+ const t = idx.table.toLowerCase(), key = columnSetKey(idx.columns);
61
+ if (!liveByTable.has(t))
62
+ liveByTable.set(t, new Set);
63
+ liveByTable.get(t).add(key);
64
+ }
65
+ const missing = [], skippedTables = new Set;
66
+ for (const decl of declared) {
67
+ const t = decl.table.toLowerCase();
68
+ if (!liveTables.has(t)) {
69
+ skippedTables.add(decl.table);
70
+ continue;
71
+ }
72
+ const key = columnSetKey(decl.columns);
73
+ if (!(liveByTable.get(t)?.has(key) ?? !1))
74
+ missing.push(decl);
75
+ }
76
+ return { supported: !0, declared, live, missing, skippedTables: [...skippedTables] };
77
+ }
78
+ async function getLiveTables(dialect) {
79
+ const { db } = await import("./utils");
80
+ let rows = [];
81
+ if (dialect === "sqlite") {
82
+ const r = await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute();
83
+ rows = (Array.isArray(r) ? r : []).map((x) => x.name);
84
+ } else if (dialect === "mysql") {
85
+ const r = await db.unsafe("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE()").execute();
86
+ rows = (Array.isArray(r) ? r : []).map((x) => x.name ?? x.TABLE_NAME);
87
+ } else if (dialect === "postgres") {
88
+ const r = await db.unsafe("SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'").execute();
89
+ rows = (Array.isArray(r) ? r : []).map((x) => x.name ?? x.tablename);
90
+ }
91
+ return new Set(rows.filter((n) => typeof n === "string" && n.length > 0).map((n) => n.toLowerCase()));
92
+ }
93
+ function columnSetKey(columns) {
94
+ return [...columns].map((c) => c.toLowerCase()).sort().join(",");
95
+ }
96
+ async function currentDialect() {
97
+ const driver = ((await import("@stacksjs/env")).env?.DB_CONNECTION ?? "sqlite").toLowerCase();
98
+ if (driver === "sqlite" || driver === "mysql" || driver === "postgres")
99
+ return driver;
100
+ return "other";
101
+ }
102
+ async function getSqliteLiveUniques(db) {
103
+ const tables = await db.unsafe("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").execute(), rows = Array.isArray(tables) ? tables : [], out = [];
104
+ for (const row of rows) {
105
+ const table = row.name;
106
+ if (!table)
107
+ continue;
108
+ if (!/^[a-z_]\w*$/i.test(table))
109
+ continue;
110
+ const indexRows = await db.unsafe(`PRAGMA index_list("${table}")`).execute();
111
+ for (const idx of Array.isArray(indexRows) ? indexRows : []) {
112
+ const r = idx;
113
+ if (Number(r.unique) !== 1 || !r.name)
114
+ continue;
115
+ if (!/^[a-z_]\w*$/i.test(r.name))
116
+ continue;
117
+ const colRows = await db.unsafe(`PRAGMA index_info("${r.name}")`).execute(), columns = (Array.isArray(colRows) ? colRows : []).map((c) => String(c.name ?? "")).filter((c) => c.length > 0);
118
+ if (columns.length > 0)
119
+ out.push({ table, name: r.name, columns });
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ async function getMysqlLiveUniques(db) {
125
+ const rows = await db.unsafe(`
126
+ SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX
127
+ FROM information_schema.STATISTICS
128
+ WHERE TABLE_SCHEMA = DATABASE()
129
+ AND NON_UNIQUE = 0
130
+ ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
131
+ `).execute();
132
+ return groupIndexRows(Array.isArray(rows) ? rows : [], (r) => ({
133
+ table: String(r.TABLE_NAME ?? r.table_name ?? ""),
134
+ name: String(r.INDEX_NAME ?? r.index_name ?? ""),
135
+ column: String(r.COLUMN_NAME ?? r.column_name ?? "")
136
+ }));
137
+ }
138
+ async function getPostgresLiveUniques(db) {
139
+ const rows = await db.unsafe(`
140
+ SELECT
141
+ t.relname AS table_name,
142
+ ix.relname AS index_name,
143
+ a.attname AS column_name,
144
+ k.ord AS seq_in_index
145
+ FROM pg_index i
146
+ JOIN pg_class t ON t.oid = i.indrelid
147
+ JOIN pg_class ix ON ix.oid = i.indexrelid
148
+ JOIN pg_namespace n ON n.oid = t.relnamespace
149
+ JOIN LATERAL unnest(i.indkey) WITH ORDINALITY AS k(attnum, ord) ON true
150
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
151
+ WHERE i.indisunique = true
152
+ AND n.nspname = 'public'
153
+ ORDER BY table_name, index_name, seq_in_index
154
+ `).execute();
155
+ return groupIndexRows(Array.isArray(rows) ? rows : [], (r) => ({
156
+ table: String(r.table_name ?? ""),
157
+ name: String(r.index_name ?? ""),
158
+ column: String(r.column_name ?? "")
159
+ }));
160
+ }
161
+ function groupIndexRows(rows, pick) {
162
+ const map = new Map;
163
+ for (const row of rows) {
164
+ const { table, name, column } = pick(row);
165
+ if (!table || !name || !column)
166
+ continue;
167
+ const key = `${table} ${name}`, existing = map.get(key);
168
+ if (existing)
169
+ existing.columns.push(column);
170
+ else
171
+ map.set(key, { table, name, columns: [column] });
172
+ }
173
+ return [...map.values()];
174
+ }