@c9up/atlas 0.2.1 → 0.2.2

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.
Binary file
@@ -24,6 +24,12 @@ import type { Plugin } from "@c9up/helix";
24
24
  /** The minimal connection the assertions need — a parameterized query runner. */
25
25
  export interface DbConnectionLike {
26
26
  query(sql: string, params?: unknown[]): Promise<Record<string, unknown>[]>;
27
+ /**
28
+ * The engine this connection speaks. Absent on a hand-rolled stub, in which
29
+ * case the assertions fall back to `?` placeholders and emit no cast, which
30
+ * is what SQLite and MySQL want.
31
+ */
32
+ dialect?: string;
27
33
  }
28
34
  /** The `ctx.db` assertion surface (AdonisJS Lucid database-assertions parity). */
29
35
  export interface DbAssertions {
@@ -1 +1 @@
1
- {"version":3,"file":"DbAssertions.d.ts","sourceRoot":"","sources":["../../src/testing/DbAssertions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAChC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;CAC3E;AA+BD,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC5B,qEAAqE;IACrE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,2CAA2C;IAC3C,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,2EAA2E;IAC3E,WAAW,CACV,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,kCAAkC;IAClC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,YAAY,CAkCvE;AAED,4EAA4E;AAC5E,wBAAgB,EAAE,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAKjD;AAID,OAAO,QAAQ,aAAa,CAAC;IAC5B,UAAU,WAAW;QACpB,EAAE,EAAE,YAAY,CAAC;KACjB;CACD"}
1
+ {"version":3,"file":"DbAssertions.d.ts","sourceRoot":"","sources":["../../src/testing/DbAssertions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAChC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAC3E;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAsHD,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC5B,qEAAqE;IACrE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1E,2CAA2C;IAC3C,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E,2EAA2E;IAC3E,WAAW,CACV,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,kCAAkC;IAClC,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C;AAED,yDAAyD;AACzD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,YAAY,CAkCvE;AAED,4EAA4E;AAC5E,wBAAgB,EAAE,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAKjD;AAID,OAAO,QAAQ,aAAa,CAAC;IAC5B,UAAU,WAAW;QACpB,EAAE,EAAE,YAAY,CAAC;KACjB;CACD"}
@@ -20,6 +20,66 @@
20
20
  * surface is re-verified against the Adonis docs when available. Model-based
21
21
  * `assertModelExists`/`assertModelMissing` (entity PK metadata) are pending.
22
22
  */
23
+ /**
24
+ * Column types Postgres will NOT compare against a text-bound parameter.
25
+ *
26
+ * A driver binds a JS string as `text`; `uuid = text` then fails with
27
+ * "operator does not exist". The cast is derived from the COLUMN's declared
28
+ * type, read from the catalog — never guessed from the value, since a text
29
+ * column holding a UUID-shaped string must keep comparing as text.
30
+ */
31
+ const PG_CASTABLE = new Set([
32
+ "uuid",
33
+ "timestamp without time zone",
34
+ "timestamp with time zone",
35
+ "date",
36
+ "time without time zone",
37
+ "time with time zone",
38
+ "numeric",
39
+ "json",
40
+ "jsonb",
41
+ "inet",
42
+ "interval",
43
+ ]);
44
+ /** Catalog lookups are per (table, connection) and never change mid-run. */
45
+ const columnTypeCache = new WeakMap();
46
+ /**
47
+ * `column → declared type` for one table, from `information_schema`.
48
+ *
49
+ * A failure here is not fatal: the assertions simply emit no cast, which is
50
+ * exactly the pre-existing behaviour. A broken catalog read must not turn a
51
+ * passing assertion into an error about introspection.
52
+ */
53
+ async function columnTypes(conn, table) {
54
+ let perConn = columnTypeCache.get(conn);
55
+ if (!perConn) {
56
+ perConn = new Map();
57
+ columnTypeCache.set(conn, perConn);
58
+ }
59
+ const cached = perConn.get(table);
60
+ if (cached)
61
+ return cached;
62
+ const types = new Map();
63
+ try {
64
+ // `table` may be schema-qualified: compare on the last segment.
65
+ const bare = table.includes(".")
66
+ ? table.slice(table.lastIndexOf(".") + 1)
67
+ : table;
68
+ const rows = await conn.query("SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1", [bare]);
69
+ for (const row of rows) {
70
+ const name = row.column_name;
71
+ const type = row.data_type;
72
+ if (typeof name === "string" && typeof type === "string") {
73
+ types.set(name, type.toLowerCase());
74
+ }
75
+ }
76
+ }
77
+ catch {
78
+ // No catalog, no cast — see the doc above.
79
+ }
80
+ perConn.set(table, types);
81
+ return types;
82
+ }
23
83
  /** Reject identifiers that could break out of the quoted context. */
24
84
  function quoteIdent(name) {
25
85
  if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(name)) {
@@ -28,12 +88,28 @@ function quoteIdent(name) {
28
88
  return `"${name.replace(/\./g, '"."')}"`;
29
89
  }
30
90
  async function countRows(conn, table, payload) {
91
+ // Postgres numbers its placeholders; MySQL and SQLite use `?`. Emitting `?`
92
+ // on Postgres produced "syntax error at or near AND" — the marker was never
93
+ // dialect-aware.
94
+ const isPostgres = conn.dialect === "postgres";
31
95
  let where = "";
32
96
  let params = [];
33
97
  if (payload) {
34
98
  const keys = Object.keys(payload);
35
99
  if (keys.length > 0) {
36
- where = ` WHERE ${keys.map((k) => `${quoteIdent(k)} = ?`).join(" AND ")}`;
100
+ const casts = isPostgres ? await columnTypes(conn, table) : undefined;
101
+ const predicates = keys.map((k, i) => {
102
+ if (!isPostgres)
103
+ return `${quoteIdent(k)} = ?`;
104
+ const declared = casts?.get(k);
105
+ const cast = declared &&
106
+ PG_CASTABLE.has(declared) &&
107
+ typeof payload[k] === "string"
108
+ ? `::${declared}`
109
+ : "";
110
+ return `${quoteIdent(k)} = $${i + 1}${cast}`;
111
+ });
112
+ where = ` WHERE ${predicates.join(" AND ")}`;
37
113
  params = keys.map((k) => payload[k]);
38
114
  }
39
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DbAssertions.js","sourceRoot":"","sources":["../../src/testing/DbAssertions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DbAssertions.js","sourceRoot":"","sources":["../../src/testing/DbAssertions.ts"],"names":[],"mappings":"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"}
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/atlas",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Atlas — Data Mapper ORM for the Ream framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,6 +26,83 @@ import type { Plugin } from "@c9up/helix";
26
26
  /** The minimal connection the assertions need — a parameterized query runner. */
27
27
  export interface DbConnectionLike {
28
28
  query(sql: string, params?: unknown[]): Promise<Record<string, unknown>[]>;
29
+ /**
30
+ * The engine this connection speaks. Absent on a hand-rolled stub, in which
31
+ * case the assertions fall back to `?` placeholders and emit no cast, which
32
+ * is what SQLite and MySQL want.
33
+ */
34
+ dialect?: string;
35
+ }
36
+
37
+ /**
38
+ * Column types Postgres will NOT compare against a text-bound parameter.
39
+ *
40
+ * A driver binds a JS string as `text`; `uuid = text` then fails with
41
+ * "operator does not exist". The cast is derived from the COLUMN's declared
42
+ * type, read from the catalog — never guessed from the value, since a text
43
+ * column holding a UUID-shaped string must keep comparing as text.
44
+ */
45
+ const PG_CASTABLE = new Set([
46
+ "uuid",
47
+ "timestamp without time zone",
48
+ "timestamp with time zone",
49
+ "date",
50
+ "time without time zone",
51
+ "time with time zone",
52
+ "numeric",
53
+ "json",
54
+ "jsonb",
55
+ "inet",
56
+ "interval",
57
+ ]);
58
+
59
+ /** Catalog lookups are per (table, connection) and never change mid-run. */
60
+ const columnTypeCache = new WeakMap<
61
+ DbConnectionLike,
62
+ Map<string, Map<string, string>>
63
+ >();
64
+
65
+ /**
66
+ * `column → declared type` for one table, from `information_schema`.
67
+ *
68
+ * A failure here is not fatal: the assertions simply emit no cast, which is
69
+ * exactly the pre-existing behaviour. A broken catalog read must not turn a
70
+ * passing assertion into an error about introspection.
71
+ */
72
+ async function columnTypes(
73
+ conn: DbConnectionLike,
74
+ table: string,
75
+ ): Promise<Map<string, string>> {
76
+ let perConn = columnTypeCache.get(conn);
77
+ if (!perConn) {
78
+ perConn = new Map();
79
+ columnTypeCache.set(conn, perConn);
80
+ }
81
+ const cached = perConn.get(table);
82
+ if (cached) return cached;
83
+
84
+ const types = new Map<string, string>();
85
+ try {
86
+ // `table` may be schema-qualified: compare on the last segment.
87
+ const bare = table.includes(".")
88
+ ? table.slice(table.lastIndexOf(".") + 1)
89
+ : table;
90
+ const rows = await conn.query(
91
+ "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1",
92
+ [bare],
93
+ );
94
+ for (const row of rows) {
95
+ const name = row.column_name;
96
+ const type = row.data_type;
97
+ if (typeof name === "string" && typeof type === "string") {
98
+ types.set(name, type.toLowerCase());
99
+ }
100
+ }
101
+ } catch {
102
+ // No catalog, no cast — see the doc above.
103
+ }
104
+ perConn.set(table, types);
105
+ return types;
29
106
  }
30
107
 
31
108
  /** Reject identifiers that could break out of the quoted context. */
@@ -41,12 +118,28 @@ async function countRows(
41
118
  table: string,
42
119
  payload?: Record<string, unknown>,
43
120
  ): Promise<number> {
121
+ // Postgres numbers its placeholders; MySQL and SQLite use `?`. Emitting `?`
122
+ // on Postgres produced "syntax error at or near AND" — the marker was never
123
+ // dialect-aware.
124
+ const isPostgres = conn.dialect === "postgres";
44
125
  let where = "";
45
126
  let params: unknown[] = [];
46
127
  if (payload) {
47
128
  const keys = Object.keys(payload);
48
129
  if (keys.length > 0) {
49
- where = ` WHERE ${keys.map((k) => `${quoteIdent(k)} = ?`).join(" AND ")}`;
130
+ const casts = isPostgres ? await columnTypes(conn, table) : undefined;
131
+ const predicates = keys.map((k, i) => {
132
+ if (!isPostgres) return `${quoteIdent(k)} = ?`;
133
+ const declared = casts?.get(k);
134
+ const cast =
135
+ declared &&
136
+ PG_CASTABLE.has(declared) &&
137
+ typeof payload[k] === "string"
138
+ ? `::${declared}`
139
+ : "";
140
+ return `${quoteIdent(k)} = $${i + 1}${cast}`;
141
+ });
142
+ where = ` WHERE ${predicates.join(" AND ")}`;
50
143
  params = keys.map((k) => payload[k]);
51
144
  }
52
145
  }