@avelonjs/postgres 0.1.0 → 0.3.0

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/src/fixtures.ts CHANGED
@@ -1,57 +1,7 @@
1
1
  import type { SQL } from 'bun'
2
+ import { ASSAY_FIXTURE_SQL } from './fixtures-sql'
2
3
 
3
- /** SQL that drops and recreates the database conformance fixtures. */
4
- export const ASSAY_FIXTURE_SQL = `
5
- DROP TABLE IF EXISTS assay_reactions CASCADE;
6
- DROP TABLE IF EXISTS assay_comments CASCADE;
7
- DROP TABLE IF EXISTS assay_posts CASCADE;
8
- DROP TABLE IF EXISTS assay_profiles CASCADE;
9
- DROP TABLE IF EXISTS assay_users CASCADE;
10
- DROP FUNCTION IF EXISTS assay_echo(jsonb);
11
-
12
- CREATE TABLE assay_users (
13
- id text PRIMARY KEY,
14
- email text NOT NULL UNIQUE,
15
- name text NOT NULL,
16
- age integer NOT NULL,
17
- nickname text
18
- );
19
-
20
- CREATE TABLE assay_profiles (
21
- id text PRIMARY KEY,
22
- user_id text NOT NULL UNIQUE REFERENCES assay_users(id) ON DELETE CASCADE,
23
- bio text NOT NULL
24
- );
25
-
26
- CREATE TABLE assay_posts (
27
- id text PRIMARY KEY,
28
- user_id text NOT NULL REFERENCES assay_users(id) ON DELETE CASCADE,
29
- title text NOT NULL,
30
- score integer NOT NULL,
31
- published_at timestamptz
32
- );
33
-
34
- CREATE TABLE assay_comments (
35
- id text PRIMARY KEY,
36
- post_id text NOT NULL REFERENCES assay_posts(id) ON DELETE CASCADE,
37
- body text NOT NULL,
38
- position integer NOT NULL
39
- );
40
-
41
- CREATE TABLE assay_reactions (
42
- id text PRIMARY KEY,
43
- comment_id text NOT NULL REFERENCES assay_comments(id) ON DELETE CASCADE,
44
- kind text NOT NULL
45
- );
46
-
47
- CREATE OR REPLACE FUNCTION assay_echo(args jsonb)
48
- RETURNS jsonb
49
- LANGUAGE sql
50
- IMMUTABLE
51
- AS $$
52
- SELECT args;
53
- $$;
54
- `
4
+ export { ASSAY_FIXTURE_SQL, ASSAY_FIXTURE_STATEMENTS } from './fixtures-sql'
55
5
 
56
6
  /** Provisions empty assay fixture tables and the `assay_echo` routine. */
57
7
  export async function resetAssayFixtures(sql: SQL): Promise<void> {
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export { bunSqlRunner } from './bun-client'
1
2
  export { compilePostgres, compileSqlPredicate, type CompiledSql } from './compile'
2
3
  export {
3
4
  createPostgresDatabase,
@@ -6,7 +7,7 @@ export {
6
7
  type PostgresDatabaseOptions,
7
8
  } from './driver'
8
9
  export { mapPostgresError, POSTGRES_ERROR_MAP } from './errors'
9
- export { ASSAY_FIXTURE_SQL, resetAssayFixtures } from './fixtures'
10
+ export { ASSAY_FIXTURE_SQL, ASSAY_FIXTURE_STATEMENTS, resetAssayFixtures } from './fixtures'
10
11
  export {
11
12
  applyMigrations,
12
13
  planMigrations,
@@ -0,0 +1,142 @@
1
+ import type { MigrationPlan, MigrationStatus } from '@avelonjs/core'
2
+ import type { CompiledSql } from './compile'
3
+ import { mapPostgresError } from './errors'
4
+ import type { SqlBatchRunner, SqlRunner } from './sql-client'
5
+
6
+ /** One driver-owned SQL migration pair. */
7
+ export interface PostgresMigration {
8
+ /** Stable migration identifier. */
9
+ id: string
10
+ /** Statements applied in order. */
11
+ up: readonly string[]
12
+ /** Statements used to roll the migration back. */
13
+ down: readonly string[]
14
+ }
15
+
16
+ const HISTORY_TABLE = 'avelon_migrations'
17
+
18
+ function statement(text: string, parameters: unknown[] = []): CompiledSql {
19
+ return { text, parameters }
20
+ }
21
+
22
+ function asDate(value: unknown): Date | undefined {
23
+ if (value instanceof Date) return value
24
+ if (typeof value === 'string' || typeof value === 'number') {
25
+ const parsed = new Date(value)
26
+ return Number.isNaN(parsed.getTime()) ? undefined : parsed
27
+ }
28
+ return undefined
29
+ }
30
+
31
+ async function ensureHistory(runner: SqlRunner): Promise<void> {
32
+ try {
33
+ await runner.unsafe(`
34
+ CREATE TABLE IF NOT EXISTS ${HISTORY_TABLE} (
35
+ id text PRIMARY KEY,
36
+ applied_at timestamptz NOT NULL DEFAULT now()
37
+ )
38
+ `)
39
+ } catch (error) {
40
+ mapPostgresError(error, 'migrations.ensureHistory')
41
+ }
42
+ }
43
+
44
+ async function appliedIds(runner: SqlRunner): Promise<Set<string>> {
45
+ await ensureHistory(runner)
46
+ try {
47
+ const result = await runner.unsafe(`SELECT id FROM ${HISTORY_TABLE}`)
48
+ return new Set(result.rows.map((row) => String(row.id)))
49
+ } catch (error) {
50
+ mapPostgresError(error, 'migrations.status')
51
+ }
52
+ }
53
+
54
+ /** Builds a pending migration plan from registered migrations and history. */
55
+ export async function planMigrations(
56
+ runner: SqlRunner,
57
+ migrations: readonly PostgresMigration[],
58
+ ): Promise<MigrationPlan> {
59
+ const applied = await appliedIds(runner)
60
+ const pending = migrations.filter((migration) => !applied.has(migration.id))
61
+ return {
62
+ id: `postgres-${pending.map((migration) => migration.id).join('+') || 'empty'}`,
63
+ migrations: pending.map((migration) => migration.id),
64
+ steps: pending.flatMap((migration) => migration.up),
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Applies every pending migration and returns the resulting statuses.
70
+ *
71
+ * Each migration's statements and its history row are known before the batch starts, so the whole
72
+ * migration reaches the runner as one atomic statement list.
73
+ */
74
+ export async function applyMigrations(
75
+ runner: SqlBatchRunner,
76
+ migrations: readonly PostgresMigration[],
77
+ ): Promise<readonly MigrationStatus[]> {
78
+ const plan = await planMigrations(runner, migrations)
79
+ for (const id of plan.migrations) {
80
+ const migration = migrations.find((entry) => entry.id === id)
81
+ if (migration === undefined) continue
82
+ try {
83
+ await runner.batch([
84
+ ...migration.up.map((text) => statement(text)),
85
+ statement(`INSERT INTO ${HISTORY_TABLE} (id) VALUES ($1)`, [id]),
86
+ ])
87
+ } catch (error) {
88
+ mapPostgresError(error, 'migrations.apply')
89
+ }
90
+ }
91
+ return statusMigrations(runner, migrations)
92
+ }
93
+
94
+ /** Rolls back the newest applied migration batches. */
95
+ export async function rollbackMigrations(
96
+ runner: SqlBatchRunner,
97
+ migrations: readonly PostgresMigration[],
98
+ steps = 1,
99
+ ): Promise<readonly MigrationStatus[]> {
100
+ await ensureHistory(runner)
101
+ const count = Number.isInteger(steps) && steps > 0 ? steps : 1
102
+ try {
103
+ const applied = await runner.unsafe(
104
+ `SELECT id, applied_at FROM ${HISTORY_TABLE} ORDER BY applied_at DESC LIMIT $1`,
105
+ [count],
106
+ )
107
+
108
+ for (const row of applied.rows) {
109
+ const id = String(row.id)
110
+ const migration = migrations.find((entry) => entry.id === id)
111
+ if (migration === undefined) continue
112
+ await runner.batch([
113
+ ...migration.down.map((text) => statement(text)),
114
+ statement(`DELETE FROM ${HISTORY_TABLE} WHERE id = $1`, [id]),
115
+ ])
116
+ }
117
+ } catch (error) {
118
+ mapPostgresError(error, 'migrations.rollback')
119
+ }
120
+ return statusMigrations(runner, migrations)
121
+ }
122
+
123
+ /** Returns applied/pending status for every registered migration. */
124
+ export async function statusMigrations(
125
+ runner: SqlRunner,
126
+ migrations: readonly PostgresMigration[],
127
+ ): Promise<readonly MigrationStatus[]> {
128
+ await ensureHistory(runner)
129
+ try {
130
+ const result = await runner.unsafe(`SELECT id, applied_at FROM ${HISTORY_TABLE}`)
131
+ const byId = new Map(result.rows.map((row) => [String(row.id), asDate(row.applied_at)]))
132
+ return migrations.map((migration) => {
133
+ if (!byId.has(migration.id)) return { id: migration.id, applied: false }
134
+ const appliedAt = byId.get(migration.id)
135
+ return appliedAt === undefined
136
+ ? { id: migration.id, applied: true }
137
+ : { id: migration.id, applied: true, appliedAt }
138
+ })
139
+ } catch (error) {
140
+ mapPostgresError(error, 'migrations.status')
141
+ }
142
+ }
package/src/migrations.ts CHANGED
@@ -1,54 +1,22 @@
1
1
  import type { SQL } from 'bun'
2
2
  import type { MigrationPlan, MigrationStatus } from '@avelonjs/core'
3
- import { mapPostgresError } from './errors'
3
+ import { bunSqlRunner } from './bun-client'
4
+ import {
5
+ applyMigrations as applyMigrationsCore,
6
+ planMigrations as planMigrationsCore,
7
+ rollbackMigrations as rollbackMigrationsCore,
8
+ statusMigrations as statusMigrationsCore,
9
+ type PostgresMigration,
10
+ } from './migrations-core'
4
11
 
5
- /** One driver-owned SQL migration pair. */
6
- export interface PostgresMigration {
7
- /** Stable migration identifier. */
8
- id: string
9
- /** Statements applied in order. */
10
- up: readonly string[]
11
- /** Statements used to roll the migration back. */
12
- down: readonly string[]
13
- }
14
-
15
- const HISTORY_TABLE = 'avelon_migrations'
16
-
17
- async function ensureHistory(sql: SQL): Promise<void> {
18
- try {
19
- await sql.unsafe(`
20
- CREATE TABLE IF NOT EXISTS ${HISTORY_TABLE} (
21
- id text PRIMARY KEY,
22
- applied_at timestamptz NOT NULL DEFAULT now()
23
- )
24
- `)
25
- } catch (error) {
26
- mapPostgresError(error, 'migrations.ensureHistory')
27
- }
28
- }
29
-
30
- async function appliedIds(sql: SQL): Promise<Set<string>> {
31
- await ensureHistory(sql)
32
- try {
33
- const rows = (await sql.unsafe(`SELECT id FROM ${HISTORY_TABLE}`)) as Array<{ id: string }>
34
- return new Set(rows.map((row) => row.id))
35
- } catch (error) {
36
- mapPostgresError(error, 'migrations.status')
37
- }
38
- }
12
+ export type { PostgresMigration } from './migrations-core'
39
13
 
40
14
  /** Builds a pending migration plan from registered migrations and history. */
41
15
  export async function planMigrations(
42
16
  sql: SQL,
43
17
  migrations: readonly PostgresMigration[],
44
18
  ): Promise<MigrationPlan> {
45
- const applied = await appliedIds(sql)
46
- const pending = migrations.filter((migration) => !applied.has(migration.id))
47
- return {
48
- id: `postgres-${pending.map((migration) => migration.id).join('+') || 'empty'}`,
49
- migrations: pending.map((migration) => migration.id),
50
- steps: pending.flatMap((migration) => migration.up),
51
- }
19
+ return planMigrationsCore(bunSqlRunner(sql), migrations)
52
20
  }
53
21
 
54
22
  /** Applies every pending migration and returns the resulting statuses. */
@@ -56,20 +24,7 @@ export async function applyMigrations(
56
24
  sql: SQL,
57
25
  migrations: readonly PostgresMigration[],
58
26
  ): Promise<readonly MigrationStatus[]> {
59
- const plan = await planMigrations(sql, migrations)
60
- for (const id of plan.migrations) {
61
- const migration = migrations.find((entry) => entry.id === id)
62
- if (migration === undefined) continue
63
- try {
64
- await sql.begin(async (tx) => {
65
- for (const statement of migration.up) await tx.unsafe(statement)
66
- await tx.unsafe(`INSERT INTO ${HISTORY_TABLE} (id) VALUES ($1)`, [id])
67
- })
68
- } catch (error) {
69
- mapPostgresError(error, 'migrations.apply')
70
- }
71
- }
72
- return statusMigrations(sql, migrations)
27
+ return applyMigrationsCore(bunSqlRunner(sql), migrations)
73
28
  }
74
29
 
75
30
  /** Rolls back the newest applied migration batches. */
@@ -78,26 +33,7 @@ export async function rollbackMigrations(
78
33
  migrations: readonly PostgresMigration[],
79
34
  steps = 1,
80
35
  ): Promise<readonly MigrationStatus[]> {
81
- await ensureHistory(sql)
82
- const count = Number.isInteger(steps) && steps > 0 ? steps : 1
83
- try {
84
- const applied = (await sql.unsafe(
85
- `SELECT id, applied_at FROM ${HISTORY_TABLE} ORDER BY applied_at DESC LIMIT $1`,
86
- [count],
87
- )) as Array<{ id: string; applied_at: Date }>
88
-
89
- for (const row of applied) {
90
- const migration = migrations.find((entry) => entry.id === row.id)
91
- if (migration === undefined) continue
92
- await sql.begin(async (tx) => {
93
- for (const statement of migration.down) await tx.unsafe(statement)
94
- await tx.unsafe(`DELETE FROM ${HISTORY_TABLE} WHERE id = $1`, [row.id])
95
- })
96
- }
97
- } catch (error) {
98
- mapPostgresError(error, 'migrations.rollback')
99
- }
100
- return statusMigrations(sql, migrations)
36
+ return rollbackMigrationsCore(bunSqlRunner(sql), migrations, steps)
101
37
  }
102
38
 
103
39
  /** Returns applied/pending status for every registered migration. */
@@ -105,19 +41,5 @@ export async function statusMigrations(
105
41
  sql: SQL,
106
42
  migrations: readonly PostgresMigration[],
107
43
  ): Promise<readonly MigrationStatus[]> {
108
- await ensureHistory(sql)
109
- try {
110
- const rows = (await sql.unsafe(
111
- `SELECT id, applied_at FROM ${HISTORY_TABLE}`,
112
- )) as Array<{ id: string; applied_at: Date }>
113
- const byId = new Map(rows.map((row) => [row.id, row.applied_at]))
114
- return migrations.map((migration) => {
115
- const appliedAt = byId.get(migration.id)
116
- return appliedAt === undefined
117
- ? { id: migration.id, applied: false }
118
- : { id: migration.id, applied: true, appliedAt }
119
- })
120
- } catch (error) {
121
- mapPostgresError(error, 'migrations.status')
122
- }
44
+ return statusMigrationsCore(bunSqlRunner(sql), migrations)
123
45
  }
@@ -0,0 +1,124 @@
1
+ import { Invalid, type QueryIR, type RelationLoad } from '@avelonjs/core'
2
+ import { mapPostgresError } from './errors'
3
+ import type { SqlRunner } from './sql-client'
4
+
5
+ /** Cached public-schema column sets keyed by table name. */
6
+ export type SchemaCache = Map<string, Set<string>>
7
+
8
+ /** Loads every public base table and its columns through any SQL runner. */
9
+ export async function loadSchemaCache(runner: SqlRunner): Promise<SchemaCache> {
10
+ try {
11
+ const result = await runner.unsafe(`
12
+ SELECT table_name, column_name
13
+ FROM information_schema.columns
14
+ WHERE table_schema = 'public'
15
+ ORDER BY table_name, ordinal_position
16
+ `)
17
+
18
+ const cache: SchemaCache = new Map()
19
+ for (const row of result.rows) {
20
+ const table = String(row.table_name)
21
+ const columns = cache.get(table) ?? new Set<string>()
22
+ columns.add(String(row.column_name))
23
+ cache.set(table, columns)
24
+ }
25
+ return cache
26
+ } catch (error) {
27
+ mapPostgresError(error, 'schema.load')
28
+ }
29
+ }
30
+
31
+ function invalid(message: string, field: string, detail: string): never {
32
+ throw new Invalid(message, { metadata: { fields: { [field]: [detail] } } })
33
+ }
34
+
35
+ function assertTable(cache: SchemaCache, table: string, field: string): Set<string> {
36
+ const columns = cache.get(table)
37
+ if (columns === undefined) {
38
+ invalid(`Unknown table ${table}.`, field, 'Unknown identifier')
39
+ }
40
+ return columns
41
+ }
42
+
43
+ function assertColumn(columns: Set<string>, table: string, column: string, field: string): void {
44
+ if (!columns.has(column)) {
45
+ invalid(`Unknown column ${column} on ${table}.`, field, `Unknown identifier: ${column}`)
46
+ }
47
+ }
48
+
49
+ function assertProjection(
50
+ columns: Set<string>,
51
+ table: string,
52
+ projection: string[] | '*',
53
+ field: string,
54
+ ): void {
55
+ if (projection === '*') return
56
+ for (const column of projection) assertColumn(columns, table, column, field)
57
+ }
58
+
59
+ function assertRelation(cache: SchemaCache, parentTable: string, relation: RelationLoad): void {
60
+ const parentColumns = assertTable(cache, parentTable, 'relations')
61
+ assertColumn(parentColumns, parentTable, relation.localKey, 'relations')
62
+ const relatedColumns = assertTable(cache, relation.table, 'relations')
63
+ assertColumn(relatedColumns, relation.table, relation.foreignKey, 'relations')
64
+ assertProjection(relatedColumns, relation.table, relation.select, 'relations')
65
+ for (const term of relation.order) {
66
+ assertColumn(relatedColumns, relation.table, term.column, 'relations')
67
+ }
68
+ for (const nested of relation.relations) assertRelation(cache, relation.table, nested)
69
+ }
70
+
71
+ /**
72
+ * Rejects unknown tables and columns before a statement is sent.
73
+ *
74
+ * Needed so empty parent row sets cannot skip a relation table that does not exist.
75
+ */
76
+ export function assertQueryAgainstSchema(cache: SchemaCache, query: QueryIR): void {
77
+ const columns = assertTable(cache, query.table, 'table')
78
+ assertProjection(columns, query.table, query.select, 'select')
79
+ if (query.returning !== undefined) {
80
+ assertProjection(columns, query.table, query.returning, 'returning')
81
+ }
82
+ for (const term of query.order) assertColumn(columns, query.table, term.column, 'order')
83
+
84
+ const checkPredicateColumns = (predicate: QueryIR['where'][number]): void => {
85
+ switch (predicate.kind) {
86
+ case 'compare':
87
+ case 'null':
88
+ case 'in':
89
+ assertColumn(columns, query.table, predicate.column, 'where')
90
+ return
91
+ case 'and':
92
+ case 'or':
93
+ predicate.predicates.forEach(checkPredicateColumns)
94
+ return
95
+ case 'not':
96
+ checkPredicateColumns(predicate.predicate)
97
+ return
98
+ case 'const':
99
+ return
100
+ }
101
+ }
102
+ query.where.forEach(checkPredicateColumns)
103
+ if (query.ward) checkPredicateColumns(query.ward)
104
+
105
+ if (query.values !== undefined) {
106
+ const rows = Array.isArray(query.values) ? query.values : [query.values]
107
+ for (const row of rows) {
108
+ for (const column of Object.keys(row)) assertColumn(columns, query.table, column, 'values')
109
+ }
110
+ }
111
+
112
+ if (query.conflict !== undefined) {
113
+ for (const column of query.conflict.columns) {
114
+ assertColumn(columns, query.table, column, 'conflict')
115
+ }
116
+ if (query.conflict.update !== '*') {
117
+ for (const column of query.conflict.update) {
118
+ assertColumn(columns, query.table, column, 'conflict')
119
+ }
120
+ }
121
+ }
122
+
123
+ for (const relation of query.relations) assertRelation(cache, query.table, relation)
124
+ }
package/src/schema.ts CHANGED
@@ -1,123 +1,11 @@
1
1
  import type { SQL } from 'bun'
2
- import { Invalid, type QueryIR, type RelationLoad } from '@avelonjs/core'
3
- import { mapPostgresError } from './errors'
2
+ import { bunSqlRunner } from './bun-client'
3
+ import { loadSchemaCache as loadSchemaCacheCore, type SchemaCache } from './schema-core'
4
4
 
5
- /** Cached public-schema column sets keyed by table name. */
6
- export type SchemaCache = Map<string, Set<string>>
5
+ export { assertQueryAgainstSchema } from './schema-core'
6
+ export type { SchemaCache } from './schema-core'
7
7
 
8
8
  /** Loads every public base table and its columns. */
9
9
  export async function loadSchemaCache(sql: SQL): Promise<SchemaCache> {
10
- try {
11
- const rows = (await sql.unsafe(`
12
- SELECT table_name, column_name
13
- FROM information_schema.columns
14
- WHERE table_schema = 'public'
15
- ORDER BY table_name, ordinal_position
16
- `)) as Array<{ table_name: string; column_name: string }>
17
-
18
- const cache: SchemaCache = new Map()
19
- for (const row of rows) {
20
- const columns = cache.get(row.table_name) ?? new Set<string>()
21
- columns.add(row.column_name)
22
- cache.set(row.table_name, columns)
23
- }
24
- return cache
25
- } catch (error) {
26
- mapPostgresError(error, 'schema.load')
27
- }
28
- }
29
-
30
- function invalid(message: string, field: string, detail: string): never {
31
- throw new Invalid(message, { metadata: { fields: { [field]: [detail] } } })
32
- }
33
-
34
- function assertTable(cache: SchemaCache, table: string, field: string): Set<string> {
35
- const columns = cache.get(table)
36
- if (columns === undefined) {
37
- invalid(`Unknown table ${table}.`, field, 'Unknown identifier')
38
- }
39
- return columns
40
- }
41
-
42
- function assertColumn(columns: Set<string>, table: string, column: string, field: string): void {
43
- if (!columns.has(column)) {
44
- invalid(`Unknown column ${column} on ${table}.`, field, `Unknown identifier: ${column}`)
45
- }
46
- }
47
-
48
- function assertProjection(
49
- columns: Set<string>,
50
- table: string,
51
- projection: string[] | '*',
52
- field: string,
53
- ): void {
54
- if (projection === '*') return
55
- for (const column of projection) assertColumn(columns, table, column, field)
56
- }
57
-
58
- function assertRelation(cache: SchemaCache, parentTable: string, relation: RelationLoad): void {
59
- const parentColumns = assertTable(cache, parentTable, 'relations')
60
- assertColumn(parentColumns, parentTable, relation.localKey, 'relations')
61
- const relatedColumns = assertTable(cache, relation.table, 'relations')
62
- assertColumn(relatedColumns, relation.table, relation.foreignKey, 'relations')
63
- assertProjection(relatedColumns, relation.table, relation.select, 'relations')
64
- for (const term of relation.order) {
65
- assertColumn(relatedColumns, relation.table, term.column, 'relations')
66
- }
67
- for (const nested of relation.relations) assertRelation(cache, relation.table, nested)
68
- }
69
-
70
- /**
71
- * Rejects unknown tables and columns before a statement is sent.
72
- *
73
- * Needed so empty parent row sets cannot skip a relation table that does not exist.
74
- */
75
- export function assertQueryAgainstSchema(cache: SchemaCache, query: QueryIR): void {
76
- const columns = assertTable(cache, query.table, 'table')
77
- assertProjection(columns, query.table, query.select, 'select')
78
- if (query.returning !== undefined) {
79
- assertProjection(columns, query.table, query.returning, 'returning')
80
- }
81
- for (const term of query.order) assertColumn(columns, query.table, term.column, 'order')
82
-
83
- const checkPredicateColumns = (predicate: QueryIR['where'][number]): void => {
84
- switch (predicate.kind) {
85
- case 'compare':
86
- case 'null':
87
- case 'in':
88
- assertColumn(columns, query.table, predicate.column, 'where')
89
- return
90
- case 'and':
91
- case 'or':
92
- predicate.predicates.forEach(checkPredicateColumns)
93
- return
94
- case 'not':
95
- checkPredicateColumns(predicate.predicate)
96
- return
97
- case 'const':
98
- return
99
- }
100
- }
101
- query.where.forEach(checkPredicateColumns)
102
- if (query.ward) checkPredicateColumns(query.ward)
103
-
104
- if (query.values !== undefined) {
105
- const rows = Array.isArray(query.values) ? query.values : [query.values]
106
- for (const row of rows) {
107
- for (const column of Object.keys(row)) assertColumn(columns, query.table, column, 'values')
108
- }
109
- }
110
-
111
- if (query.conflict !== undefined) {
112
- for (const column of query.conflict.columns) {
113
- assertColumn(columns, query.table, column, 'conflict')
114
- }
115
- if (query.conflict.update !== '*') {
116
- for (const column of query.conflict.update) {
117
- assertColumn(columns, query.table, column, 'conflict')
118
- }
119
- }
120
- }
121
-
122
- for (const relation of query.relations) assertRelation(cache, query.table, relation)
10
+ return loadSchemaCacheCore(bunSqlRunner(sql))
123
11
  }
@@ -0,0 +1,28 @@
1
+ import type { CompiledSql } from './compile'
2
+
3
+ /**
4
+ * Normalized statement result.
5
+ *
6
+ * Bun returns an array carrying a non-enumerable `count`; Neon returns `{ rows, rowCount }`. Both
7
+ * client adapters flatten to this shape so the execution algorithm never inspects a vendor result.
8
+ */
9
+ export interface SqlRows {
10
+ /** Rows the statement produced, owned by the caller and safe to mutate. */
11
+ rows: Record<string, unknown>[]
12
+ /** Rows the statement affected, used for writes without `RETURNING`. */
13
+ count: number
14
+ }
15
+
16
+ /** Statement runner the vendor-neutral query execution needs. */
17
+ export interface SqlRunner {
18
+ /** Runs one parameterized statement and returns its normalized result. */
19
+ unsafe(text: string, parameters?: readonly unknown[]): Promise<SqlRows>
20
+ /** Releases the underlying connection. Absent on a runner that owns none. */
21
+ close?(): Promise<void>
22
+ }
23
+
24
+ /** A runner that can also apply a fully known statement list atomically. */
25
+ export interface SqlBatchRunner extends SqlRunner {
26
+ /** Applies every statement in one transaction, or none of them. */
27
+ batch(statements: readonly CompiledSql[]): Promise<void>
28
+ }
package/src/sql.ts ADDED
@@ -0,0 +1,15 @@
1
+ export { compilePostgres, compileSqlPredicate, type CompiledSql } from './compile'
2
+ export { mapPostgresError, POSTGRES_ERROR_MAP } from './errors'
3
+ export { executeQueryIR, executeRpc, type RoundTripCounter } from './execute'
4
+ export { ASSAY_FIXTURE_SQL, ASSAY_FIXTURE_STATEMENTS } from './fixtures-sql'
5
+ export {
6
+ applyMigrations,
7
+ planMigrations,
8
+ rollbackMigrations,
9
+ statusMigrations,
10
+ type PostgresMigration,
11
+ } from './migrations-core'
12
+ export { combinedPredicate, normalizePredicate } from './normalize'
13
+ export { assertQueryAgainstSchema, loadSchemaCache, type SchemaCache } from './schema-core'
14
+ export type { SqlBatchRunner, SqlRows, SqlRunner } from './sql-client'
15
+ export { assertIdentifier, validateQueryIR } from './validate'