@stacksjs/database 0.70.300 → 0.70.302

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/dialect.d.ts CHANGED
@@ -31,6 +31,15 @@ export declare function toSqlIntrospectionDialect(dialect: string): SqlIntrospec
31
31
  export declare function indexSqlForDialect(statement: string, dialect: string): string;
32
32
  /** Whether an error is the expected result of replaying bare MySQL index DDL. */
33
33
  export declare function isDuplicateIndexError(error: unknown): boolean;
34
+ /**
35
+ * True when an `ALTER TABLE ... ADD COLUMN` failed only because the column is
36
+ * already there.
37
+ *
38
+ * SQLite says "duplicate column name", MySQL "Duplicate column name",
39
+ * Postgres "column ... of relation ... already exists". Adding a column that
40
+ * exists is the success case for a reconciliation pass, not a failure.
41
+ */
42
+ export declare function isDuplicateColumnError(error: unknown): boolean;
34
43
  /** Every dialect with a capability row, for CLI help and validation messages. */
35
44
  export declare function knownDialects(): string[];
36
45
  /**
package/dist/dialect.js CHANGED
@@ -1 +1 @@
1
- import process from"node:process";const CAPABILITIES={sqlite:{dialect:"sqlite",wire:"sqlite",queryBuilderDialect:"sqlite",identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0},mysql:{dialect:"mysql",wire:"mysql",queryBuilderDialect:"mysql",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},singlestore:{dialect:"singlestore",wire:"mysql",queryBuilderDialect:"singlestore",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},vitess:{dialect:"vitess",wire:"mysql",queryBuilderDialect:"vitess",defaultPort:15306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!1,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!0,supportsCreateIndexIfNotExists:!1},postgres:{dialect:"postgres",wire:"postgres",queryBuilderDialect:"postgres",defaultPort:5432,identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0}};export function isVitessSharded(explicit){if(typeof explicit==="boolean")return explicit;const raw=(typeof explicit==="string"?explicit:process.env.DB_VITESS_SHARDED)?.trim().toLowerCase();if(raw===void 0||raw==="")return!0;return!["0","false","no","off"].includes(raw)}export function dialectCapabilities(dialect,options={}){const caps=CAPABILITIES[dialect]??CAPABILITIES.sqlite;if(dialect!=="vitess"||isVitessSharded(options.vitessSharded))return caps;return{...CAPABILITIES.mysql,dialect:"vitess",queryBuilderDialect:"vitess",defaultPort:15306}}export function isKnownDialect(dialect){return dialect in CAPABILITIES}export function toSqlIntrospectionDialect(dialect){if(!isKnownDialect(dialect))return"other";return dialectCapabilities(dialect).wire}export function indexSqlForDialect(statement,dialect){if(dialectCapabilities(dialect).supportsCreateIndexIfNotExists)return statement;return statement.replace(/^(\s*CREATE\s+(?:UNIQUE\s+)?INDEX)\s+IF\s+NOT\s+EXISTS\s+/i,"$1 ")}export function isDuplicateIndexError(error){const message=error instanceof Error?error.message:String(error);return/duplicate key name|already exists/i.test(message)}export function knownDialects(){return Object.keys(CAPABILITIES)}export function isMysqlWire(dialect){return dialectCapabilities(dialect).wire==="mysql"}export function isPostgresWire(dialect){return dialectCapabilities(dialect).wire==="postgres"}export function toQueryBuilderDialect(dialect){return dialectCapabilities(dialect).queryBuilderDialect}
1
+ import process from"node:process";const CAPABILITIES={sqlite:{dialect:"sqlite",wire:"sqlite",queryBuilderDialect:"sqlite",identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0},mysql:{dialect:"mysql",wire:"mysql",queryBuilderDialect:"mysql",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},singlestore:{dialect:"singlestore",wire:"mysql",queryBuilderDialect:"singlestore",defaultPort:3306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!1},vitess:{dialect:"vitess",wire:"mysql",queryBuilderDialect:"vitess",defaultPort:15306,identifierQuote:"`",supportsForeignKeys:!1,supportsAutoIncrement:!1,supportsAtomicMultiTableTransactions:!1,requiresOnlineDdl:!0,supportsCreateIndexIfNotExists:!1},postgres:{dialect:"postgres",wire:"postgres",queryBuilderDialect:"postgres",defaultPort:5432,identifierQuote:'"',supportsForeignKeys:!0,supportsAutoIncrement:!0,supportsAtomicMultiTableTransactions:!0,requiresOnlineDdl:!1,supportsCreateIndexIfNotExists:!0}};export function isVitessSharded(explicit){if(typeof explicit==="boolean")return explicit;const raw=(typeof explicit==="string"?explicit:process.env.DB_VITESS_SHARDED)?.trim().toLowerCase();if(raw===void 0||raw==="")return!0;return!["0","false","no","off"].includes(raw)}export function dialectCapabilities(dialect,options={}){const caps=CAPABILITIES[dialect]??CAPABILITIES.sqlite;if(dialect!=="vitess"||isVitessSharded(options.vitessSharded))return caps;return{...CAPABILITIES.mysql,dialect:"vitess",queryBuilderDialect:"vitess",defaultPort:15306}}export function isKnownDialect(dialect){return dialect in CAPABILITIES}export function toSqlIntrospectionDialect(dialect){if(!isKnownDialect(dialect))return"other";return dialectCapabilities(dialect).wire}export function indexSqlForDialect(statement,dialect){if(dialectCapabilities(dialect).supportsCreateIndexIfNotExists)return statement;return statement.replace(/^(\s*CREATE\s+(?:UNIQUE\s+)?INDEX)\s+IF\s+NOT\s+EXISTS\s+/i,"$1 ")}export function isDuplicateIndexError(error){const message=error instanceof Error?error.message:String(error);return/duplicate key name|already exists/i.test(message)}export function isDuplicateColumnError(error){const message=error instanceof Error?error.message:String(error);return/duplicate column|already exists/i.test(message)}export function knownDialects(){return Object.keys(CAPABILITIES)}export function isMysqlWire(dialect){return dialectCapabilities(dialect).wire==="mysql"}export function isPostgresWire(dialect){return dialectCapabilities(dialect).wire==="postgres"}export function toQueryBuilderDialect(dialect){return dialectCapabilities(dialect).queryBuilderDialect}
@@ -86,6 +86,14 @@ export declare function commentableUpvotesTableSql(sql: SqlHelpers): string;
86
86
  * supported, {@link migrateTraitTables} drops the clause and tolerates the
87
87
  * duplicate-index error on replay instead.
88
88
  */
89
+ /**
90
+ * Columns the indexes below depend on, for tables that may predate them.
91
+ *
92
+ * Only additive: a column that is missing gets added with a default, and one
93
+ * that is present is left alone. Nothing here drops or retypes, so replaying
94
+ * it against an up-to-date database is a no-op.
95
+ */
96
+ export declare function traitTableColumnGuarantees(sql: SqlHelpers): { table: string, column: string, definition: string }[];
89
97
  export declare function traitTableIndexSql(): string[];
90
98
  /**
91
99
  * The `<table>_likes` tables to create, one per model that sets `likeable`.
@@ -1,4 +1,4 @@
1
- import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{indexSqlForDialect,isDuplicateIndexError}from"./dialect";export{indexSqlForDialect}from"./dialect";function getDbDriver(){return process.env.DB_CONNECTION||envVars.DB_CONNECTION||"sqlite"}function createdAt(sql){return`created_at ${sql.datetime}`}function updatedAt(sql){return`updated_at ${sql.nullableTimestamp}`}export function traitTableNames(){return["commentables","taggables","categorizables","commentable_upvotes","taggable_models","categorizable_models"]}export const UNSCOPED_OWNER_ID=0;export function commentablesTableSql(sql){const{pkColumn,boolTrue}=sql;return`CREATE TABLE IF NOT EXISTS commentables (
1
+ import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";import{db}from"./utils";import{sqlHelpers}from"./sql-helpers";import{indexSqlForDialect,isDuplicateColumnError,isDuplicateIndexError}from"./dialect";export{indexSqlForDialect}from"./dialect";function getDbDriver(){return process.env.DB_CONNECTION||envVars.DB_CONNECTION||"sqlite"}function createdAt(sql){return`created_at ${sql.datetime}`}function updatedAt(sql){return`updated_at ${sql.nullableTimestamp}`}export function traitTableNames(){return["commentables","taggables","categorizables","commentable_upvotes","taggable_models","categorizable_models"]}export const UNSCOPED_OWNER_ID=0;export function commentablesTableSql(sql){const{pkColumn,boolTrue}=sql;return`CREATE TABLE IF NOT EXISTS commentables (
2
2
  ${pkColumn},
3
3
  title VARCHAR(255) NOT NULL,
4
4
  body TEXT NOT NULL,
@@ -58,4 +58,4 @@ import{log}from"@stacksjs/logging";import{env as envVars}from"@stacksjs/env";imp
58
58
  upvoteable_id INTEGER NOT NULL,
59
59
  upvoteable_type VARCHAR(255) NOT NULL,
60
60
  ${createdAt(sql)}
61
- )`}export function traitTableIndexSql(){return["CREATE INDEX IF NOT EXISTS commentables_owner_index ON commentables (commentables_type, commentables_id)","CREATE INDEX IF NOT EXISTS commentables_status_index ON commentables (status)","CREATE UNIQUE INDEX IF NOT EXISTS taggables_owner_slug_unique ON taggables (taggable_type, taggable_id, slug)","CREATE UNIQUE INDEX IF NOT EXISTS categorizables_owner_slug_unique ON categorizables (categorizable_type, categorizable_id, slug)","CREATE INDEX IF NOT EXISTS commentable_upvotes_target_index ON commentable_upvotes (upvoteable_type, upvoteable_id)","CREATE UNIQUE INDEX IF NOT EXISTS commentable_upvotes_user_unique ON commentable_upvotes (upvoteable_type, upvoteable_id, user_id)","CREATE UNIQUE INDEX IF NOT EXISTS taggable_models_unique ON taggable_models (tag_id, taggable_id, taggable_type)","CREATE UNIQUE INDEX IF NOT EXISTS categorizable_models_unique ON categorizable_models (category_id, categorizable_id, categorizable_type)"]}export async function likeableTargets(){const{path}=await import("@stacksjs/path"),{globSync}=await import("@stacksjs/storage"),{getTableName}=await import("@stacksjs/orm"),{getLikeableForeignKey,getUpvoteTableName}=await import("./drivers/helpers"),modelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),targets=new Map;for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model?.traits?.likeable)continue;const tableName=await getTableName(model,modelFile);if(!tableName)continue;const table=getUpvoteTableName(model,tableName);if(!table||!/^[a-z_]\w*$/i.test(table))continue;const foreignKey=getLikeableForeignKey(model,tableName);if(!/^[a-z_]\w*$/i.test(foreignKey))continue;targets.set(table,{table,foreignKey})}return[...targets.values()]}export async function migrateTraitTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver);if(options.verbose)log.info(`Creating polymorphic trait tables for ${dbDriver}...`);try{if(options.verbose)log.info("Creating commentables table...");await db.unsafe(commentablesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggables table...");await db.unsafe(taggablesTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizables table...");await db.unsafe(categorizablesTableSql(sql)).execute();if(options.verbose)log.info("Creating commentable_upvotes table...");await db.unsafe(commentableUpvotesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggable_models pivot...");await db.unsafe(taggableModelsTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizable_models pivot...");await db.unsafe(categorizableModelsTableSql(sql)).execute();try{for(const{table,foreignKey}of await likeableTargets()){if(options.verbose)log.info(`Creating ${table} table...`);await db.unsafe(likesTableSql(sql,table,foreignKey)).execute()}}catch(error){log.debug(`[trait-tables] Skipped likeable tables: ${error instanceof Error?error.message:String(error)}`)}for(const statement of traitTableIndexSql())try{await db.unsafe(indexSqlForDialect(statement,dbDriver)).execute()}catch(error){if(!isDuplicateIndexError(error))throw error}if(options.verbose)log.success("Polymorphic trait tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create polymorphic trait tables: ${message}`);return{success:!1,error:message}}}
61
+ )`}export function traitTableColumnGuarantees(sql){return[{table:"taggables",column:"taggable_id",definition:`INTEGER NOT NULL DEFAULT ${UNSCOPED_OWNER_ID}`},{table:"categorizables",column:"categorizable_id",definition:`INTEGER NOT NULL DEFAULT ${UNSCOPED_OWNER_ID}`}]}export function traitTableIndexSql(){return["CREATE INDEX IF NOT EXISTS commentables_owner_index ON commentables (commentables_type, commentables_id)","CREATE INDEX IF NOT EXISTS commentables_status_index ON commentables (status)","CREATE UNIQUE INDEX IF NOT EXISTS taggables_owner_slug_unique ON taggables (taggable_type, taggable_id, slug)","CREATE UNIQUE INDEX IF NOT EXISTS categorizables_owner_slug_unique ON categorizables (categorizable_type, categorizable_id, slug)","CREATE INDEX IF NOT EXISTS commentable_upvotes_target_index ON commentable_upvotes (upvoteable_type, upvoteable_id)","CREATE UNIQUE INDEX IF NOT EXISTS commentable_upvotes_user_unique ON commentable_upvotes (upvoteable_type, upvoteable_id, user_id)","CREATE UNIQUE INDEX IF NOT EXISTS taggable_models_unique ON taggable_models (tag_id, taggable_id, taggable_type)","CREATE UNIQUE INDEX IF NOT EXISTS categorizable_models_unique ON categorizable_models (category_id, categorizable_id, categorizable_type)"]}export async function likeableTargets(){const{path}=await import("@stacksjs/path"),{globSync}=await import("@stacksjs/storage"),{getTableName}=await import("@stacksjs/orm"),{getLikeableForeignKey,getUpvoteTableName}=await import("./drivers/helpers"),modelFiles=globSync([path.userModelsPath("*.ts"),path.storagePath("framework/defaults/app/Models/**/*.ts")],{absolute:!0}),targets=new Map;for(const modelFile of modelFiles){let model;try{model=(await import(modelFile)).default}catch{continue}if(!model?.traits?.likeable)continue;const tableName=await getTableName(model,modelFile);if(!tableName)continue;const table=getUpvoteTableName(model,tableName);if(!table||!/^[a-z_]\w*$/i.test(table))continue;const foreignKey=getLikeableForeignKey(model,tableName);if(!/^[a-z_]\w*$/i.test(foreignKey))continue;targets.set(table,{table,foreignKey})}return[...targets.values()]}export async function migrateTraitTables(options={}){const dbDriver=getDbDriver(),sql=sqlHelpers(dbDriver);if(options.verbose)log.info(`Creating polymorphic trait tables for ${dbDriver}...`);try{if(options.verbose)log.info("Creating commentables table...");await db.unsafe(commentablesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggables table...");await db.unsafe(taggablesTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizables table...");await db.unsafe(categorizablesTableSql(sql)).execute();if(options.verbose)log.info("Creating commentable_upvotes table...");await db.unsafe(commentableUpvotesTableSql(sql)).execute();if(options.verbose)log.info("Creating taggable_models pivot...");await db.unsafe(taggableModelsTableSql(sql)).execute();if(options.verbose)log.info("Creating categorizable_models pivot...");await db.unsafe(categorizableModelsTableSql(sql)).execute();try{for(const{table,foreignKey}of await likeableTargets()){if(options.verbose)log.info(`Creating ${table} table...`);await db.unsafe(likesTableSql(sql,table,foreignKey)).execute()}}catch(error){log.debug(`[trait-tables] Skipped likeable tables: ${error instanceof Error?error.message:String(error)}`)}for(const{table,column,definition}of traitTableColumnGuarantees(sql))try{await db.unsafe(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`).execute();if(options.verbose)log.info(`Added missing ${table}.${column}`)}catch(error){if(!isDuplicateColumnError(error))throw error}for(const statement of traitTableIndexSql())try{await db.unsafe(indexSqlForDialect(statement,dbDriver)).execute()}catch(error){if(!isDuplicateIndexError(error))throw error}if(options.verbose)log.success("Polymorphic trait tables created");return{success:!0}}catch(error){const message=error instanceof Error?error.message:String(error);log.error(`Failed to create polymorphic trait tables: ${message}`);return{success:!1,error:message}}}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/database",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.300",
5
+ "version": "0.70.302",
6
6
  "description": "The Stacks database integration.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -61,19 +61,19 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "@stacksjs/ts-validation": "^0.5.0",
64
- "bun-query-builder": "^0.2.23",
64
+ "bun-query-builder": "^0.2.24",
65
65
  "dynamodb-tooling": "^0.3.2"
66
66
  },
67
67
  "devDependencies": {
68
- "@stacksjs/cli": "0.70.300",
69
- "@stacksjs/config": "0.70.300",
70
- "@stacksjs/logging": "0.70.300",
71
- "@stacksjs/router": "0.70.300",
68
+ "@stacksjs/cli": "0.70.302",
69
+ "@stacksjs/config": "0.70.302",
70
+ "@stacksjs/logging": "0.70.302",
71
+ "@stacksjs/router": "0.70.302",
72
72
  "better-dx": "^0.2.17",
73
- "@stacksjs/path": "0.70.300",
74
- "@stacksjs/query-builder": "0.70.300",
75
- "@stacksjs/storage": "0.70.300",
76
- "@stacksjs/strings": "0.70.300",
77
- "@stacksjs/utils": "0.70.300"
73
+ "@stacksjs/path": "0.70.302",
74
+ "@stacksjs/query-builder": "0.70.302",
75
+ "@stacksjs/storage": "0.70.302",
76
+ "@stacksjs/strings": "0.70.302",
77
+ "@stacksjs/utils": "0.70.302"
78
78
  }
79
79
  }