@vertz/db 0.2.11 → 0.2.13

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/README.md CHANGED
@@ -82,8 +82,7 @@ d.textArray() // TEXT[] → string[]
82
82
  d.integerArray() // INTEGER[] → number[]
83
83
  d.enum('status', ['active', 'inactive']) // ENUM → 'active' | 'inactive'
84
84
 
85
- // Multi-tenancy
86
- d.tenant(orgsTable) // UUID FK to tenant root → string
85
+ // Multi-tenancy — see d.model() options below
87
86
  ```
88
87
 
89
88
  ### Column Modifiers
@@ -131,6 +130,64 @@ const users = d.table('users', {
131
130
  });
132
131
  ```
133
132
 
133
+ ### Indexes
134
+
135
+ Define indexes on tables via the `indexes` option:
136
+
137
+ ```typescript
138
+ const posts = d.table('posts', {
139
+ id: d.uuid().primary(),
140
+ title: d.text(),
141
+ status: d.text(),
142
+ authorId: d.uuid(),
143
+ }, {
144
+ indexes: [
145
+ d.index('title'), // basic index
146
+ d.index('authorId', { unique: true }), // unique index
147
+ d.index(['status', 'authorId']), // composite index
148
+ ],
149
+ });
150
+ ```
151
+
152
+ #### Index Types (PostgreSQL)
153
+
154
+ PostgreSQL supports multiple index types via the `type` option. The default is `btree`.
155
+
156
+ ```typescript
157
+ d.index('title', { type: 'gin' }) // GIN — full-text search, arrays, JSONB
158
+ d.index('location', { type: 'gist' }) // GiST — geometric/spatial data
159
+ d.index('email', { type: 'hash' }) // Hash — equality-only lookups
160
+ d.index('created_at', { type: 'brin' }) // BRIN — large sorted datasets
161
+ d.index('name', { type: 'btree' }) // B-tree (default)
162
+ ```
163
+
164
+ Available types: `'btree' | 'hash' | 'gin' | 'gist' | 'brin'`
165
+
166
+ When targeting SQLite, non-btree index types are silently ignored (SQLite only supports B-tree). A console warning is emitted during migration generation.
167
+
168
+ #### Partial Indexes
169
+
170
+ Use the `where` option to create partial indexes that only index rows matching a condition:
171
+
172
+ ```typescript
173
+ d.index('email', {
174
+ unique: true,
175
+ where: "status = 'active'",
176
+ })
177
+ // SQL: CREATE UNIQUE INDEX ... ON ... ("email") WHERE status = 'active';
178
+ ```
179
+
180
+ Partial indexes are supported on both PostgreSQL and SQLite.
181
+
182
+ #### Index Options
183
+
184
+ | Option | Type | Description |
185
+ |--------|------|-------------|
186
+ | `unique` | `boolean` | Create a UNIQUE index |
187
+ | `type` | `IndexType` | Index type — PostgreSQL only (`btree`, `hash`, `gin`, `gist`, `brin`) |
188
+ | `where` | `string` | WHERE clause for partial indexes |
189
+ | `name` | `string` | Custom index name (auto-generated if omitted) |
190
+
134
191
  ### Annotations
135
192
 
136
193
  Column annotations control visibility and mutability across the stack:
@@ -476,11 +533,15 @@ const orgsTable = d.table('organizations', {
476
533
  const usersTable = d.table('users', {
477
534
  id: d.uuid().primary(),
478
535
  email: d.email(),
479
- orgId: d.tenant(orgsTable), // scopes this table to a tenant
536
+ orgId: d.uuid(),
480
537
  });
481
538
 
482
539
  const orgsModel = d.model(orgsTable);
483
- const usersModel = d.model(usersTable);
540
+ const usersModel = d.model(
541
+ usersTable,
542
+ { organization: d.ref.one(() => orgsTable, 'orgId') },
543
+ { tenant: 'organization' }, // scopes this model to a tenant via relation
544
+ );
484
545
 
485
546
  const tenantGraph = computeTenantGraph({ organizations: orgsModel, users: usersModel });
486
547
 
@@ -32,11 +32,6 @@ interface ColumnMetadata {
32
32
  readonly _annotations: Record<string, true>;
33
33
  readonly isReadOnly: boolean;
34
34
  readonly isAutoUpdate: boolean;
35
- readonly isTenant: boolean;
36
- readonly references: {
37
- readonly table: string;
38
- readonly column: string;
39
- } | null;
40
35
  readonly check: string | null;
41
36
  readonly defaultValue?: unknown;
42
37
  readonly format?: string;
@@ -87,18 +82,15 @@ interface ColumnBuilder<
87
82
  check(sql: string): ColumnBuilder<TType, Omit<TMeta, "check"> & {
88
83
  readonly check: string;
89
84
  }>;
90
- references(table: string, column?: string): ColumnBuilder<TType, Omit<TMeta, "references"> & {
91
- readonly references: {
92
- readonly table: string;
93
- readonly column: string;
94
- };
95
- }>;
96
85
  }
97
86
  type InferColumnType<C> = C extends ColumnBuilder<infer T, ColumnMetadata> ? T : never;
87
+ type IndexType = "btree" | "hash" | "gin" | "gist" | "brin";
98
88
  interface IndexDef {
99
89
  readonly columns: readonly string[];
100
90
  readonly name?: string;
101
91
  readonly unique?: boolean;
92
+ readonly type?: IndexType;
93
+ readonly where?: string;
102
94
  }
103
95
  /** A record of column builders -- the shape passed to d.table(). */
104
96
  type ColumnRecord = Record<string, ColumnBuilder<unknown, ColumnMetadata>>;
package/dist/d1/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createD1Adapter,
3
3
  createD1Driver
4
- } from "../shared/chunk-pnk6yzjv.js";
4
+ } from "../shared/chunk-ndxe1h28.js";
5
5
  export {
6
6
  createD1Driver,
7
7
  createD1Adapter
package/dist/index.d.ts CHANGED
@@ -72,11 +72,6 @@ interface ColumnMetadata {
72
72
  readonly _annotations: Record<string, true>;
73
73
  readonly isReadOnly: boolean;
74
74
  readonly isAutoUpdate: boolean;
75
- readonly isTenant: boolean;
76
- readonly references: {
77
- readonly table: string;
78
- readonly column: string;
79
- } | null;
80
75
  readonly check: string | null;
81
76
  readonly defaultValue?: unknown;
82
77
  readonly format?: string;
@@ -127,12 +122,6 @@ interface ColumnBuilder<
127
122
  check(sql: string): ColumnBuilder<TType, Omit<TMeta, "check"> & {
128
123
  readonly check: string;
129
124
  }>;
130
- references(table: string, column?: string): ColumnBuilder<TType, Omit<TMeta, "references"> & {
131
- readonly references: {
132
- readonly table: string;
133
- readonly column: string;
134
- };
135
- }>;
136
125
  }
137
126
  type InferColumnType<C> = C extends ColumnBuilder<infer T, ColumnMetadata> ? T : never;
138
127
  type DefaultMeta<TSqlType extends string> = {
@@ -144,8 +133,6 @@ type DefaultMeta<TSqlType extends string> = {
144
133
  readonly _annotations: {};
145
134
  readonly isReadOnly: false;
146
135
  readonly isAutoUpdate: false;
147
- readonly isTenant: false;
148
- readonly references: null;
149
136
  readonly check: null;
150
137
  };
151
138
  /** Metadata for varchar columns — carries the length constraint. */
@@ -184,24 +171,6 @@ type SerialMeta = {
184
171
  readonly _annotations: {};
185
172
  readonly isReadOnly: false;
186
173
  readonly isAutoUpdate: false;
187
- readonly isTenant: false;
188
- readonly references: null;
189
- readonly check: null;
190
- };
191
- type TenantMeta = {
192
- readonly sqlType: "uuid";
193
- readonly primary: false;
194
- readonly unique: false;
195
- readonly nullable: false;
196
- readonly hasDefault: false;
197
- readonly _annotations: {};
198
- readonly isReadOnly: false;
199
- readonly isAutoUpdate: false;
200
- readonly isTenant: true;
201
- readonly references: {
202
- readonly table: string;
203
- readonly column: string;
204
- };
205
174
  readonly check: null;
206
175
  };
207
176
  interface ThroughDef<TJoin extends TableDef<ColumnRecord> = TableDef<ColumnRecord>> {
@@ -221,10 +190,19 @@ interface RelationDef<
221
190
  interface ManyRelationDef<TTarget extends TableDef<ColumnRecord> = TableDef<ColumnRecord>> extends RelationDef<TTarget, "many"> {
222
191
  through<TJoin extends TableDef<ColumnRecord>>(joinTable: () => TJoin, thisKey: string, thatKey: string): RelationDef<TTarget, "many">;
223
192
  }
193
+ type IndexType = "btree" | "hash" | "gin" | "gist" | "brin";
224
194
  interface IndexDef {
225
195
  readonly columns: readonly string[];
226
196
  readonly name?: string;
227
197
  readonly unique?: boolean;
198
+ readonly type?: IndexType;
199
+ readonly where?: string;
200
+ }
201
+ interface IndexOptions {
202
+ name?: string;
203
+ unique?: boolean;
204
+ type?: IndexType;
205
+ where?: string;
228
206
  }
229
207
  /** A record of column builders -- the shape passed to d.table(). */
230
208
  type ColumnRecord = Record<string, ColumnBuilder<unknown, ColumnMetadata>>;
@@ -906,30 +884,34 @@ interface D1PreparedStatement2 {
906
884
  }>;
907
885
  }
908
886
  interface TenantGraph {
909
- /** The tenant root table name (e.g., "organizations"). Null if no tenant columns exist. */
887
+ /** The tenant root table key (e.g., "organizations"). Null if no tenant declarations exist. */
910
888
  readonly root: string | null;
911
- /** Tables with a direct d.tenant() column pointing to the root. */
889
+ /** Model keys with a direct { tenant } declaration pointing to the root. */
912
890
  readonly directlyScoped: readonly string[];
913
- /** Tables reachable from directly scoped tables via .references() chains. */
891
+ /** Model keys reachable from scoped models via relation chains. */
914
892
  readonly indirectlyScoped: readonly string[];
915
- /** Tables marked with .shared(). */
893
+ /** Model keys whose tables are marked with .shared(). */
916
894
  readonly shared: readonly string[];
917
895
  }
918
- interface TableRegistryEntry {
896
+ interface ModelRegistryEntry {
919
897
  readonly table: TableDef<ColumnRecord>;
920
- readonly relations: Record<string, unknown>;
898
+ readonly relations: Record<string, RelationDef>;
899
+ readonly _tenant?: string | null;
921
900
  }
922
- type TableRegistry = Record<string, TableRegistryEntry>;
901
+ type ModelRegistry = Record<string, ModelRegistryEntry>;
923
902
  /**
924
- * Analyzes a table registry to compute the tenant scoping graph.
903
+ * Analyzes a model registry to compute the tenant scoping graph.
925
904
  *
926
- * 1. Finds the tenant root the table that tenant columns point to.
927
- * 2. Classifies tables as directly scoped (has d.tenant()), indirectly scoped
928
- * (references a scoped table via .references()), or shared (.shared()).
929
- * 3. Tables that are none of the above are unscoped the caller should
905
+ * Fully relation-derivedreads _tenant from model options and follows
906
+ * relation chains for indirect scoping. Does NOT scan column metadata.
907
+ *
908
+ * 1. Finds the tenant root the table that tenant relations point to.
909
+ * 2. Classifies models as directly scoped (has { tenant } option),
910
+ * indirectly scoped (has a relation to a scoped model), or shared (.shared()).
911
+ * 3. Models that are none of the above are unscoped — the caller should
930
912
  * log a notice for those.
931
913
  */
932
- declare function computeTenantGraph(registry: TableRegistry): TenantGraph;
914
+ declare function computeTenantGraph(registry: ModelRegistry): TenantGraph;
933
915
  interface PoolConfig {
934
916
  /** Maximum number of connections in the pool. */
935
917
  readonly max?: number;
@@ -1124,8 +1106,8 @@ type DatabaseClient<TModels extends Record<string, ModelEntry>> = { readonly [K
1124
1106
  * Each model in the registry becomes a property on the returned client
1125
1107
  * with all CRUD methods typed for that specific model.
1126
1108
  *
1127
- * Computes the tenant graph at creation time from d.tenant() metadata,
1128
- * traversing references to find indirect tenant paths.
1109
+ * Computes the tenant graph at creation time from model-level { tenant }
1110
+ * options, traversing relations to find indirect tenant paths.
1129
1111
  * Logs notices for tables without tenant paths and not .shared().
1130
1112
  *
1131
1113
  * When `url` is provided and `_queryFn` is NOT provided, creates a real
@@ -1214,6 +1196,37 @@ declare function parseMigrationName(filename: string): {
1214
1196
  timestamp: number;
1215
1197
  name: string;
1216
1198
  } | null;
1199
+ interface SchemaLike<T> {
1200
+ parse(value: unknown): {
1201
+ ok: true;
1202
+ data: T;
1203
+ } | {
1204
+ ok: false;
1205
+ error: Error;
1206
+ };
1207
+ }
1208
+ interface ModelSchemas<TTable extends TableDef<ColumnRecord>> {
1209
+ readonly response: SchemaLike<TTable["$response"]>;
1210
+ readonly createInput: SchemaLike<TTable["$create_input"]>;
1211
+ readonly updateInput: SchemaLike<TTable["$update_input"]>;
1212
+ }
1213
+ interface ModelDef<
1214
+ TTable extends TableDef<ColumnRecord> = TableDef<ColumnRecord>,
1215
+ TRelations extends Record<string, RelationDef> = {}
1216
+ > {
1217
+ readonly table: TTable;
1218
+ readonly relations: TRelations;
1219
+ readonly schemas: ModelSchemas<TTable>;
1220
+ readonly _tenant: string | null;
1221
+ }
1222
+ interface ModelOptions<TRelations extends Record<string, RelationDef>> {
1223
+ /**
1224
+ * The relation that defines the tenant boundary for this model.
1225
+ * Must reference a key in the relations record. The referenced relation's
1226
+ * target table is the tenant root.
1227
+ */
1228
+ readonly tenant?: Extract<keyof TRelations, string>;
1229
+ }
1217
1230
  interface ColumnSnapshot {
1218
1231
  type: string;
1219
1232
  nullable: boolean;
@@ -1226,6 +1239,8 @@ interface IndexSnapshot {
1226
1239
  columns: string[];
1227
1240
  name?: string;
1228
1241
  unique?: boolean;
1242
+ type?: IndexType;
1243
+ where?: string;
1229
1244
  }
1230
1245
  interface ForeignKeySnapshot {
1231
1246
  column: string;
@@ -1243,7 +1258,7 @@ interface SchemaSnapshot {
1243
1258
  tables: Record<string, TableSnapshot>;
1244
1259
  enums: Record<string, string[]>;
1245
1260
  }
1246
- declare function createSnapshot(tables: TableDef<ColumnRecord>[]): SchemaSnapshot;
1261
+ declare function createSnapshot(entries: (TableDef<ColumnRecord> | ModelDef)[]): SchemaSnapshot;
1247
1262
  type ChangeType = "table_added" | "table_removed" | "column_added" | "column_removed" | "column_altered" | "column_renamed" | "index_added" | "index_removed" | "enum_added" | "enum_removed" | "enum_altered";
1248
1263
  interface CollisionInfo {
1249
1264
  existingName: string;
@@ -1399,28 +1414,6 @@ interface PostgresDriver extends DbDriver {
1399
1414
  * @returns A PostgresDriver with queryFn, close(), and isHealthy()
1400
1415
  */
1401
1416
  declare function createPostgresDriver(url: string, pool?: PoolConfig): PostgresDriver;
1402
- interface SchemaLike<T> {
1403
- parse(value: unknown): {
1404
- ok: true;
1405
- data: T;
1406
- } | {
1407
- ok: false;
1408
- error: Error;
1409
- };
1410
- }
1411
- interface ModelSchemas<TTable extends TableDef<ColumnRecord>> {
1412
- readonly response: SchemaLike<TTable["$response"]>;
1413
- readonly createInput: SchemaLike<TTable["$create_input"]>;
1414
- readonly updateInput: SchemaLike<TTable["$update_input"]>;
1415
- }
1416
- interface ModelDef<
1417
- TTable extends TableDef<ColumnRecord> = TableDef<ColumnRecord>,
1418
- TRelations extends Record<string, RelationDef> = {}
1419
- > {
1420
- readonly table: TTable;
1421
- readonly relations: TRelations;
1422
- readonly schemas: ModelSchemas<TTable>;
1423
- }
1424
1417
  interface EnumSchemaLike<T extends readonly string[]> {
1425
1418
  readonly values: T;
1426
1419
  }
@@ -1455,9 +1448,8 @@ declare const d: {
1455
1448
  TName extends string,
1456
1449
  const TValues extends readonly [string, ...string[]]
1457
1450
  >(name: TName, schema: EnumSchemaLike<TValues>): ColumnBuilder<TValues[number], EnumMeta<TName, TValues>>;
1458
- tenant(targetTable: TableDef<ColumnRecord>): ColumnBuilder<string, TenantMeta>;
1459
1451
  table<TColumns extends ColumnRecord>(name: string, columns: TColumns, options?: TableOptions): TableDef<TColumns>;
1460
- index(columns: string | string[]): IndexDef;
1452
+ index(columns: string | string[], options?: IndexOptions): IndexDef;
1461
1453
  ref: {
1462
1454
  one<TTarget extends TableDef<ColumnRecord>>(target: () => TTarget, foreignKey: string): RelationDef<TTarget, "one">;
1463
1455
  many<TTarget extends TableDef<ColumnRecord>>(target: () => TTarget, foreignKey: string): RelationDef<TTarget, "many">;
@@ -1473,8 +1465,17 @@ declare const d: {
1473
1465
  TTable extends TableDef<ColumnRecord>,
1474
1466
  TRelations extends Record<string, RelationDef>
1475
1467
  >(table: TTable, relations: TRelations): ModelDef<TTable, TRelations>;
1468
+ model<
1469
+ TTable extends TableDef<ColumnRecord>,
1470
+ TRelations extends Record<string, RelationDef>
1471
+ >(table: TTable, relations: TRelations, options: ModelOptions<TRelations>): ModelDef<TTable, TRelations>;
1476
1472
  };
1477
1473
  /**
1474
+ * Validate index definitions against a target dialect.
1475
+ * Returns warning messages for unsupported features.
1476
+ */
1477
+ declare function validateIndexes(tables: Record<string, TableSnapshot>, dialect: "postgres" | "sqlite"): string[];
1478
+ /**
1478
1479
  * Convenience utility for creating a frozen bag of annotation constants.
1479
1480
  *
1480
1481
  * Usage:
@@ -1654,4 +1655,4 @@ type StrictKeys<
1654
1655
  TAllowed extends string,
1655
1656
  TTable extends string
1656
1657
  > = TRecord extends Record<string, unknown> ? { [K in keyof TRecord] : K extends TAllowed ? TRecord[K] : InvalidColumn<K & string, TTable> } : TRecord;
1657
- export { toWriteError, toReadError, resolveErrorCode, reset, push, parsePgError, parseMigrationName, migrateStatus, migrateDev, migrateDeploy, generateId, formatDiagnostic, explainError, diagnoseError, detectSchemaDrift, defineAnnotations, defaultSqliteDialect, defaultPostgresDialect, dbErrorToHttpError, d, createSqliteDriver2 as createSqliteDriver, createSqliteAdapter, createSnapshot, createRegistry, createPostgresDriver, createEnumRegistry, createDb, createDatabaseBridgeAdapter, createD1Driver, createD1Adapter, computeTenantGraph, baseline, WriteError, VarcharMeta, ValidateKeys, UpdateInput, UniqueConstraintErrorOptions, UniqueConstraintError, TenantMeta, TenantGraph, TableDef, StrictKeys, SqliteDialect, SqliteAdapterOptions, SelectOption, SelectNarrow, SchemaSnapshot, SchemaLike, ResetResult, ResetOptions, RenameSuggestion, RelationDef, RegisteredEnum, ReadError, QueryResult, PushResult, PushOptions, PostgresDriver, PostgresDialect, PoolConfig, PgErrorInput, PgCodeToName, OrderByType, NotNullErrorOptions, NotNullError, NotFoundError, ModelSchemas, ModelEntry, ModelDelegate, ModelDef, MixedSelectError, MigrationQueryFn, MigrationInfo, MigrationFile, MigrationError2 as MigrationError, MigrateStatusResult, MigrateStatusOptions, MigrateDevResult, MigrateDevOptions, MigrateDeployResult, MigrateDeployOptions, ListOptions, JsonbValidator, InvalidRelation, InvalidFilterType, InvalidColumn, InsertInput, InferColumnType, IndexDef, IncludeResolve, IncludeOption, HttpErrorResponse, FormatMeta, ForeignKeyErrorOptions, ForeignKeyError, FindResult, FindOptions, FilterType, EnumMeta, EntityDbAdapter, DriftEntry, Dialect, DiagnosticResult, DecimalMeta, DbQueryError, DbNotFoundError, DbErrorJson, DbErrorCodeValue, DbErrorCodeName, DbErrorCode, DbErrorBase, DbError, DbDriver, DbConstraintError, DbConnectionError, DatabaseInternals, DatabaseClient, Database, D1PreparedStatement, D1DatabaseBinding, D1AdapterOptions, CreateDbOptions, ConnectionPoolExhaustedError, ConnectionError, ColumnTypeMeta, ColumnMetadata, ColumnBuilder, CodeChange, CheckConstraintErrorOptions, CheckConstraintError, BaselineResult, BaselineOptions };
1658
+ export { validateIndexes, toWriteError, toReadError, resolveErrorCode, reset, push, parsePgError, parseMigrationName, migrateStatus, migrateDev, migrateDeploy, generateId, formatDiagnostic, explainError, diagnoseError, detectSchemaDrift, defineAnnotations, defaultSqliteDialect, defaultPostgresDialect, dbErrorToHttpError, d, createSqliteDriver2 as createSqliteDriver, createSqliteAdapter, createSnapshot, createRegistry, createPostgresDriver, createEnumRegistry, createDb, createDatabaseBridgeAdapter, createD1Driver, createD1Adapter, computeTenantGraph, baseline, WriteError, VarcharMeta, ValidateKeys, UpdateInput, UniqueConstraintErrorOptions, UniqueConstraintError, TenantGraph, TableDef, StrictKeys, SqliteDialect, SqliteAdapterOptions, SelectOption, SelectNarrow, SchemaSnapshot, SchemaLike, ResetResult, ResetOptions, RenameSuggestion, RelationDef, RegisteredEnum, ReadError, QueryResult, PushResult, PushOptions, PostgresDriver, PostgresDialect, PoolConfig, PgErrorInput, PgCodeToName, OrderByType, NotNullErrorOptions, NotNullError, NotFoundError, ModelSchemas, ModelOptions, ModelEntry, ModelDelegate, ModelDef, MixedSelectError, MigrationQueryFn, MigrationInfo, MigrationFile, MigrationError2 as MigrationError, MigrateStatusResult, MigrateStatusOptions, MigrateDevResult, MigrateDevOptions, MigrateDeployResult, MigrateDeployOptions, ListOptions, JsonbValidator, InvalidRelation, InvalidFilterType, InvalidColumn, InsertInput, InferColumnType, IndexType, IndexOptions, IndexDef, IncludeResolve, IncludeOption, HttpErrorResponse, FormatMeta, ForeignKeyErrorOptions, ForeignKeyError, FindResult, FindOptions, FilterType, EnumMeta, EntityDbAdapter, DriftEntry, Dialect, DiagnosticResult, DecimalMeta, DbQueryError, DbNotFoundError, DbErrorJson, DbErrorCodeValue, DbErrorCodeName, DbErrorCode, DbErrorBase, DbError, DbDriver, DbConstraintError, DbConnectionError, DatabaseInternals, DatabaseClient, Database, D1PreparedStatement, D1DatabaseBinding, D1AdapterOptions, CreateDbOptions, ConnectionPoolExhaustedError, ConnectionError, ColumnTypeMeta, ColumnMetadata, ColumnBuilder, CodeChange, CheckConstraintErrorOptions, CheckConstraintError, BaselineResult, BaselineOptions };