shapecraft 1.0.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/CLAUDE.md +227 -0
- package/README.md +22 -0
- package/apps/cli/node_modules/.bin/prettier +21 -0
- package/apps/cli/node_modules/.bin/tsc +21 -0
- package/apps/cli/node_modules/.bin/tsserver +21 -0
- package/apps/cli/node_modules/.bin/tsx +21 -0
- package/apps/cli/node_modules/.bin/vitest +21 -0
- package/apps/cli/package.json +47 -0
- package/apps/cli/src/index.ts +98 -0
- package/apps/cli/tsconfig.cjs.json +10 -0
- package/apps/cli/tsconfig.esm.json +10 -0
- package/apps/cli/tsconfig.json +22 -0
- package/package.json +16 -0
- package/packages/core/node_modules/.bin/prettier +21 -0
- package/packages/core/node_modules/.bin/tsc +21 -0
- package/packages/core/node_modules/.bin/tsserver +21 -0
- package/packages/core/node_modules/.bin/tsx +21 -0
- package/packages/core/node_modules/.bin/vitest +21 -0
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/core/package.json +44 -0
- package/packages/core/src/common/array.test.ts +19 -0
- package/packages/core/src/common/array.ts +15 -0
- package/packages/core/src/common/index.ts +5 -0
- package/packages/core/src/common/is.ts +23 -0
- package/packages/core/src/common/object.ts +35 -0
- package/packages/core/src/common/phantom.ts +1 -0
- package/packages/core/src/common/result.ts +43 -0
- package/packages/core/src/common/string.ts +28 -0
- package/packages/core/src/common/types.ts +34 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/shape/annotate.ts +139 -0
- package/packages/core/src/shape/annotation.ts +47 -0
- package/packages/core/src/shape/base.ts +71 -0
- package/packages/core/src/shape/builder.test.ts +728 -0
- package/packages/core/src/shape/builder.ts +475 -0
- package/packages/core/src/shape/error.ts +4 -0
- package/packages/core/src/shape/index.ts +3 -0
- package/packages/core/src/shape/number.ts +118 -0
- package/packages/core/src/shape/shape.test.ts +792 -0
- package/packages/core/src/shape/shape.ts +377 -0
- package/packages/core/src/shape/tags.ts +14 -0
- package/packages/core/src/shape/transforms/index.ts +3 -0
- package/packages/core/src/shape/transforms/json-schema/index.ts +2 -0
- package/packages/core/src/shape/transforms/json-schema/transform.test.ts +850 -0
- package/packages/core/src/shape/transforms/json-schema/transform.ts +882 -0
- package/packages/core/src/shape/transforms/json-schema/types.ts +132 -0
- package/packages/core/src/shape/transforms/sql/dialects/dialect.ts +89 -0
- package/packages/core/src/shape/transforms/sql/dialects/index.ts +14 -0
- package/packages/core/src/shape/transforms/sql/dialects/postgres.ts +392 -0
- package/packages/core/src/shape/transforms/sql/dialects/sqlite.ts +333 -0
- package/packages/core/src/shape/transforms/sql/from-sql.test.ts +704 -0
- package/packages/core/src/shape/transforms/sql/from-sql.ts +210 -0
- package/packages/core/src/shape/transforms/sql/index.ts +3 -0
- package/packages/core/src/shape/transforms/sql/options.ts +6 -0
- package/packages/core/src/shape/transforms/sql/parser/check-decoder.ts +457 -0
- package/packages/core/src/shape/transforms/sql/parser/create-domain.ts +105 -0
- package/packages/core/src/shape/transforms/sql/parser/create-table.ts +809 -0
- package/packages/core/src/shape/transforms/sql/parser/create-type.ts +91 -0
- package/packages/core/src/shape/transforms/sql/parser/cursor.ts +179 -0
- package/packages/core/src/shape/transforms/sql/parser/default-decoder.ts +129 -0
- package/packages/core/src/shape/transforms/sql/parser/lexer.ts +289 -0
- package/packages/core/src/shape/transforms/sql/parser/pg-types.ts +247 -0
- package/packages/core/src/shape/transforms/sql/parser/sqlite-types.ts +103 -0
- package/packages/core/src/shape/transforms/sql/parser/statements.ts +127 -0
- package/packages/core/src/shape/transforms/sql/parser/type-spec.ts +159 -0
- package/packages/core/src/shape/transforms/sql/transform.sqlite.test.ts +448 -0
- package/packages/core/src/shape/transforms/sql/transform.test.ts +880 -0
- package/packages/core/src/shape/transforms/sql/transform.ts +295 -0
- package/packages/core/src/shape/transforms/typescript/index.ts +1 -0
- package/packages/core/src/shape/transforms/typescript/transform.ts +211 -0
- package/packages/core/src/shape/tuple.test.ts +171 -0
- package/packages/core/src/shape/validate.ts +413 -0
- package/packages/core/tsconfig.cjs.json +11 -0
- package/packages/core/tsconfig.esm.json +10 -0
- package/packages/core/tsconfig.json +23 -0
- package/packages/samples/node_modules/.bin/prettier +21 -0
- package/packages/samples/node_modules/.bin/tsc +21 -0
- package/packages/samples/node_modules/.bin/tsserver +21 -0
- package/packages/samples/node_modules/.bin/tsx +21 -0
- package/packages/samples/node_modules/.bin/vitest +21 -0
- package/packages/samples/package.json +47 -0
- package/packages/samples/src/blog.ts +49 -0
- package/packages/samples/src/config.ts +50 -0
- package/packages/samples/src/ecommerce.ts +65 -0
- package/packages/samples/src/embeddings.ts +43 -0
- package/packages/samples/src/events.ts +52 -0
- package/packages/samples/src/geometry.ts +62 -0
- package/packages/samples/src/index.ts +9 -0
- package/packages/samples/src/relational.ts +17 -0
- package/packages/samples/src/tuples.ts +67 -0
- package/packages/samples/src/user.ts +9 -0
- package/packages/samples/tsconfig.cjs.json +11 -0
- package/packages/samples/tsconfig.esm.json +10 -0
- package/packages/samples/tsconfig.json +23 -0
- package/pnpm-workspace.yaml +3 -0
- package/test-data/json-schema/address.json +35 -0
- package/test-data/json-schema/array-of-things.json +36 -0
- package/test-data/json-schema/basic.json +21 -0
- package/test-data/json-schema/blog-post.json +29 -0
- package/test-data/json-schema/calendar.json +48 -0
- package/test-data/json-schema/complex-object-with-nested-properties.json +41 -0
- package/test-data/json-schema/ecommerce-complex.json +344 -0
- package/test-data/json-schema/ecommerce-system.json +27 -0
- package/test-data/json-schema/enumerated-values.json +11 -0
- package/test-data/json-schema/fstab-entry.json +92 -0
- package/test-data/json-schema/geographical-location.json +20 -0
- package/test-data/json-schema/health-record.json +41 -0
- package/test-data/json-schema/job-posting.json +33 -0
- package/test-data/json-schema/movie.json +35 -0
- package/test-data/json-schema/regular-expression-pattern.json +12 -0
- package/test-data/json-schema/user-profile.json +33 -0
- package/test-data/sql/ecommerce.sql +641 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { NumberTag } from "../../tags";
|
|
2
|
+
|
|
3
|
+
export type JSONSchemaTypeName =
|
|
4
|
+
| "string" //
|
|
5
|
+
| "number"
|
|
6
|
+
| "integer"
|
|
7
|
+
| "boolean"
|
|
8
|
+
| "object"
|
|
9
|
+
| "array"
|
|
10
|
+
| "null";
|
|
11
|
+
|
|
12
|
+
export interface ShapecraftMeta {
|
|
13
|
+
kind?:
|
|
14
|
+
| "date"
|
|
15
|
+
| "binary"
|
|
16
|
+
| "undefined"
|
|
17
|
+
| "vector"
|
|
18
|
+
| "range"
|
|
19
|
+
| "tuple"
|
|
20
|
+
| undefined;
|
|
21
|
+
numberTag?: NumberTag | undefined;
|
|
22
|
+
vectorDims?: number | undefined;
|
|
23
|
+
tupleLen?: number | undefined;
|
|
24
|
+
unique?: boolean | undefined;
|
|
25
|
+
primary?: boolean | undefined;
|
|
26
|
+
foreign?: { table: string; column: string } | undefined;
|
|
27
|
+
optional?: boolean | undefined;
|
|
28
|
+
anchor?: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type JSONSchemaType =
|
|
32
|
+
| string //
|
|
33
|
+
| number
|
|
34
|
+
| boolean
|
|
35
|
+
| JSONSchemaObject
|
|
36
|
+
| JSONSchemaArray
|
|
37
|
+
| null;
|
|
38
|
+
|
|
39
|
+
export interface JSONSchemaObject {
|
|
40
|
+
[key: string]: JSONSchemaType;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface JSONSchemaArray extends Array<JSONSchemaType> {}
|
|
44
|
+
|
|
45
|
+
export type JSONSchemaVersion = string;
|
|
46
|
+
|
|
47
|
+
export type JSONSchemaDefinition = JSONSchema | boolean;
|
|
48
|
+
export interface JSONSchema {
|
|
49
|
+
$id?: string | undefined;
|
|
50
|
+
$ref?: string | undefined;
|
|
51
|
+
$schema?: JSONSchemaVersion | undefined;
|
|
52
|
+
$comment?: string | undefined;
|
|
53
|
+
$anchor?: string | undefined;
|
|
54
|
+
|
|
55
|
+
$defs?:
|
|
56
|
+
| {
|
|
57
|
+
[key: string]: JSONSchemaDefinition;
|
|
58
|
+
}
|
|
59
|
+
| undefined;
|
|
60
|
+
|
|
61
|
+
type?: JSONSchemaTypeName | JSONSchemaTypeName[] | undefined;
|
|
62
|
+
enum?: JSONSchemaType[] | undefined;
|
|
63
|
+
const?: JSONSchemaType | undefined;
|
|
64
|
+
|
|
65
|
+
multipleOf?: number | undefined;
|
|
66
|
+
maximum?: number | undefined;
|
|
67
|
+
exclusiveMaximum?: number | undefined;
|
|
68
|
+
minimum?: number | undefined;
|
|
69
|
+
exclusiveMinimum?: number | undefined;
|
|
70
|
+
|
|
71
|
+
maxLength?: number | undefined;
|
|
72
|
+
minLength?: number | undefined;
|
|
73
|
+
pattern?: string | undefined;
|
|
74
|
+
|
|
75
|
+
items?: JSONSchemaDefinition | JSONSchemaDefinition[] | undefined;
|
|
76
|
+
prefixItems?: JSONSchemaDefinition[] | undefined;
|
|
77
|
+
additionalItems?: JSONSchemaDefinition | undefined;
|
|
78
|
+
maxItems?: number | undefined;
|
|
79
|
+
minItems?: number | undefined;
|
|
80
|
+
uniqueItems?: boolean | undefined;
|
|
81
|
+
contains?: JSONSchemaDefinition | undefined;
|
|
82
|
+
|
|
83
|
+
maxProperties?: number | undefined;
|
|
84
|
+
minProperties?: number | undefined;
|
|
85
|
+
required?: string[] | undefined;
|
|
86
|
+
properties?:
|
|
87
|
+
| {
|
|
88
|
+
[key: string]: JSONSchemaDefinition;
|
|
89
|
+
}
|
|
90
|
+
| undefined;
|
|
91
|
+
patternProperties?:
|
|
92
|
+
| {
|
|
93
|
+
[key: string]: JSONSchemaDefinition;
|
|
94
|
+
}
|
|
95
|
+
| undefined;
|
|
96
|
+
additionalProperties?: JSONSchemaDefinition | undefined;
|
|
97
|
+
dependencies?:
|
|
98
|
+
| {
|
|
99
|
+
[key: string]: JSONSchemaDefinition | string[];
|
|
100
|
+
}
|
|
101
|
+
| undefined;
|
|
102
|
+
propertyNames?: JSONSchemaDefinition | undefined;
|
|
103
|
+
|
|
104
|
+
if?: JSONSchemaDefinition | undefined;
|
|
105
|
+
then?: JSONSchemaDefinition | undefined;
|
|
106
|
+
else?: JSONSchemaDefinition | undefined;
|
|
107
|
+
|
|
108
|
+
allOf?: JSONSchemaDefinition[] | undefined;
|
|
109
|
+
anyOf?: JSONSchemaDefinition[] | undefined;
|
|
110
|
+
oneOf?: JSONSchemaDefinition[] | undefined;
|
|
111
|
+
not?: JSONSchemaDefinition | undefined;
|
|
112
|
+
|
|
113
|
+
format?: string | undefined;
|
|
114
|
+
|
|
115
|
+
contentMediaType?: string | undefined;
|
|
116
|
+
contentEncoding?: string | undefined;
|
|
117
|
+
|
|
118
|
+
definitions?:
|
|
119
|
+
| {
|
|
120
|
+
[key: string]: JSONSchemaDefinition;
|
|
121
|
+
}
|
|
122
|
+
| undefined;
|
|
123
|
+
|
|
124
|
+
title?: string | undefined;
|
|
125
|
+
description?: string | undefined;
|
|
126
|
+
default?: JSONSchemaType | undefined;
|
|
127
|
+
readOnly?: boolean | undefined;
|
|
128
|
+
writeOnly?: boolean | undefined;
|
|
129
|
+
examples?: JSONSchemaType | undefined;
|
|
130
|
+
|
|
131
|
+
"x-shapecraft"?: ShapecraftMeta | undefined;
|
|
132
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { R } from "../../../../common/result";
|
|
2
|
+
import { shapes } from "../../../shape";
|
|
3
|
+
import { SQLDialect } from "../options";
|
|
4
|
+
|
|
5
|
+
export type Prelude = { name: string; sql: string };
|
|
6
|
+
|
|
7
|
+
export type ColumnDef = {
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
notNull: boolean;
|
|
11
|
+
primary: boolean;
|
|
12
|
+
unique: boolean;
|
|
13
|
+
identity: boolean;
|
|
14
|
+
defaultExpr?: string;
|
|
15
|
+
references?: string;
|
|
16
|
+
generated?: { expression: string; stored: boolean };
|
|
17
|
+
checks: string[];
|
|
18
|
+
comments: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type TypeRendering = {
|
|
22
|
+
type: string;
|
|
23
|
+
checks: string[];
|
|
24
|
+
prelude: Prelude[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type FormatDefault =
|
|
28
|
+
| { ok: true; expr: string }
|
|
29
|
+
| { ok: false; reason: string };
|
|
30
|
+
|
|
31
|
+
export interface Dialect {
|
|
32
|
+
readonly name: SQLDialect;
|
|
33
|
+
|
|
34
|
+
// True iff `anno.schema` is meaningful in this dialect. SQLite has no schemas.
|
|
35
|
+
readonly supportsSchema: boolean;
|
|
36
|
+
|
|
37
|
+
// Trailing tokens emitted after the closing paren of a CREATE TABLE
|
|
38
|
+
// (e.g. " STRICT" for SQLite). Empty string for postgres.
|
|
39
|
+
readonly tableTrailer: string;
|
|
40
|
+
|
|
41
|
+
// Convert a column's shape into a dialect-specific type string + per-type
|
|
42
|
+
// CHECK fragments + any prelude statements (named ENUM types, helper
|
|
43
|
+
// functions, etc.). Handles per-shape concerns only — primary/unique/default
|
|
44
|
+
// /foreign/autoIncrement are applied uniformly by convertField afterwards.
|
|
45
|
+
renderType(
|
|
46
|
+
name: string,
|
|
47
|
+
shape: shapes.Shape,
|
|
48
|
+
): R.Result<TypeRendering, string[]>;
|
|
49
|
+
|
|
50
|
+
// Validate and apply autoIncrement annotation to a ColumnDef. Some dialects
|
|
51
|
+
// (SQLite) require autoIncrement columns to also be PRIMARY KEY and have a
|
|
52
|
+
// specific integer affinity; this hook lets the dialect enforce that.
|
|
53
|
+
applyAutoIncrement(
|
|
54
|
+
name: string,
|
|
55
|
+
shape: shapes.Shape,
|
|
56
|
+
col: ColumnDef,
|
|
57
|
+
): string[];
|
|
58
|
+
|
|
59
|
+
// Format a literal default value into an SQL expression appropriate for the
|
|
60
|
+
// dialect. Casts (`::timestamptz`, `::jsonb`) are postgres-only.
|
|
61
|
+
formatDefault(v: unknown, columnType: string): FormatDefault;
|
|
62
|
+
|
|
63
|
+
// Validate the top-level mapping's annotations against dialect constraints.
|
|
64
|
+
// Returns an array of error messages — SQLite errors here on anno.schema.
|
|
65
|
+
validateMappingAnno(anno: shapes.Shape["anno"], title: string): string[];
|
|
66
|
+
|
|
67
|
+
// Render schema-creation prelude (e.g. CREATE SCHEMA IF NOT EXISTS "x";).
|
|
68
|
+
// SQLite returns the empty string and validateMappingAnno will have caught
|
|
69
|
+
// the misuse upstream.
|
|
70
|
+
schemaPrelude(name: string): string;
|
|
71
|
+
|
|
72
|
+
// Render a qualified or bare table name.
|
|
73
|
+
qualifiedTable(schema: string | undefined, title: string): string;
|
|
74
|
+
|
|
75
|
+
// Render a column definition line (no leading indent, no trailing comma).
|
|
76
|
+
// Dialects may special-case (e.g. SQLite collapses INTEGER+identity+primary
|
|
77
|
+
// into `INTEGER PRIMARY KEY AUTOINCREMENT`).
|
|
78
|
+
renderColumn(c: ColumnDef): string;
|
|
79
|
+
|
|
80
|
+
// Final validation pass after all columns are converted but before SQL is
|
|
81
|
+
// rendered. SQLite uses this to enforce single-PK constraints around
|
|
82
|
+
// AUTOINCREMENT columns.
|
|
83
|
+
finalizeColumns(cols: ColumnDef[]): string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const quoteIdent = (name: string): string =>
|
|
87
|
+
`"${name.replace(/"/g, '""')}"`;
|
|
88
|
+
|
|
89
|
+
export const quoteLit = (s: string): string => `'${s.replace(/'/g, "''")}'`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SQLDialect } from "../options";
|
|
2
|
+
import { Dialect } from "./dialect";
|
|
3
|
+
import { postgresDialect } from "./postgres";
|
|
4
|
+
import { sqliteDialect } from "./sqlite";
|
|
5
|
+
|
|
6
|
+
export type { Dialect, ColumnDef, Prelude } from "./dialect";
|
|
7
|
+
export { quoteIdent, quoteLit } from "./dialect";
|
|
8
|
+
export { renderReferentialActions } from "./postgres";
|
|
9
|
+
|
|
10
|
+
export const getDialect = (name: SQLDialect): Dialect => {
|
|
11
|
+
if (name === "postgres") return postgresDialect;
|
|
12
|
+
if (name === "sqlite") return sqliteDialect;
|
|
13
|
+
throw new Error(`unknown dialect: ${name}`);
|
|
14
|
+
};
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { R } from "../../../../common/result";
|
|
2
|
+
import { ReferentialAction } from "../../../annotation";
|
|
3
|
+
import { shapes } from "../../../shape";
|
|
4
|
+
import { NumberTag } from "../../../tags";
|
|
5
|
+
import {
|
|
6
|
+
ColumnDef,
|
|
7
|
+
Dialect,
|
|
8
|
+
FormatDefault,
|
|
9
|
+
Prelude,
|
|
10
|
+
quoteIdent,
|
|
11
|
+
quoteLit,
|
|
12
|
+
TypeRendering,
|
|
13
|
+
} from "./dialect";
|
|
14
|
+
|
|
15
|
+
const INT_RANGES: Record<string, { minimum?: number; maximum?: number }> = {
|
|
16
|
+
int8: { minimum: -128, maximum: 127 },
|
|
17
|
+
uint8: { minimum: 0, maximum: 255 },
|
|
18
|
+
int16: { minimum: -32768, maximum: 32767 },
|
|
19
|
+
uint16: { minimum: 0, maximum: 65535 },
|
|
20
|
+
int32: { minimum: -2_147_483_648, maximum: 2_147_483_647 },
|
|
21
|
+
uint32: { minimum: 0, maximum: 4_294_967_295 },
|
|
22
|
+
uint64: { minimum: 0 },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const INT_PG_TYPE: Record<string, string> = {
|
|
26
|
+
int: "bigint",
|
|
27
|
+
int8: "smallint",
|
|
28
|
+
uint8: "smallint",
|
|
29
|
+
int16: "smallint",
|
|
30
|
+
uint16: "integer",
|
|
31
|
+
int32: "integer",
|
|
32
|
+
uint32: "bigint",
|
|
33
|
+
int64: "bigint",
|
|
34
|
+
uint64: "numeric(20,0)",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const FLOAT_PG_TYPE: Record<string, string> = {
|
|
38
|
+
number: "double precision",
|
|
39
|
+
float: "real",
|
|
40
|
+
float32: "real",
|
|
41
|
+
float64: "double precision",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isIntTag = (tag: NumberTag): boolean => tag in INT_PG_TYPE;
|
|
45
|
+
|
|
46
|
+
const ARRAY_IS_UNIQUE_FN = "shapecraft_array_is_unique";
|
|
47
|
+
const ARRAY_IS_UNIQUE_FN_SQL =
|
|
48
|
+
`CREATE OR REPLACE FUNCTION ${ARRAY_IS_UNIQUE_FN}(anyarray) RETURNS boolean\n` +
|
|
49
|
+
` LANGUAGE sql IMMUTABLE PARALLEL SAFE\n` +
|
|
50
|
+
` AS $$ SELECT cardinality($1) = cardinality(ARRAY(SELECT DISTINCT unnest($1))) $$;`;
|
|
51
|
+
|
|
52
|
+
const pgTypeRange = (
|
|
53
|
+
pgType: string,
|
|
54
|
+
): { minimum?: number; maximum?: number } => {
|
|
55
|
+
if (pgType === "smallint") return { minimum: -32768, maximum: 32767 };
|
|
56
|
+
if (pgType === "integer")
|
|
57
|
+
return { minimum: -2_147_483_648, maximum: 2_147_483_647 };
|
|
58
|
+
return {};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const formatNumberLit = (n: number): string => {
|
|
62
|
+
if (!isFinite(n)) return `${quoteLit(String(n))}::double precision`;
|
|
63
|
+
return String(n);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const REFERENTIAL_ACTION_SQL: Record<ReferentialAction, string> = {
|
|
67
|
+
cascade: "CASCADE",
|
|
68
|
+
restrict: "RESTRICT",
|
|
69
|
+
setNull: "SET NULL",
|
|
70
|
+
setDefault: "SET DEFAULT",
|
|
71
|
+
noAction: "NO ACTION",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const renderReferentialActions = (foreign: {
|
|
75
|
+
onDelete?: ReferentialAction;
|
|
76
|
+
onUpdate?: ReferentialAction;
|
|
77
|
+
}): string => {
|
|
78
|
+
const parts: string[] = [];
|
|
79
|
+
if (foreign.onDelete !== undefined) {
|
|
80
|
+
parts.push(`ON DELETE ${REFERENTIAL_ACTION_SQL[foreign.onDelete]}`);
|
|
81
|
+
}
|
|
82
|
+
if (foreign.onUpdate !== undefined) {
|
|
83
|
+
parts.push(`ON UPDATE ${REFERENTIAL_ACTION_SQL[foreign.onUpdate]}`);
|
|
84
|
+
}
|
|
85
|
+
return parts.length > 0 ? " " + parts.join(" ") : "";
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const rangePgType = (shape: shapes.ShapeRange): string => {
|
|
89
|
+
const item = shape.input;
|
|
90
|
+
if (item.type === "date") {
|
|
91
|
+
if (shape.anno.format === "pgtype:tsrange") return "tsrange";
|
|
92
|
+
if (shape.anno.format === "pgtype:daterange") return "daterange";
|
|
93
|
+
return "tstzrange";
|
|
94
|
+
}
|
|
95
|
+
if (item.type === "number") {
|
|
96
|
+
if (item.tag === "int32" || item.tag === "int16") return "int4range";
|
|
97
|
+
if (item.tag === "int64" || item.tag === "int") return "int8range";
|
|
98
|
+
return "numrange";
|
|
99
|
+
}
|
|
100
|
+
return "tstzrange";
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const arrayElementPgType = (s: shapes.Shape): string | null => {
|
|
104
|
+
if (s.type === "string") {
|
|
105
|
+
if (s.literal) return null;
|
|
106
|
+
if (s.anno.max !== undefined) return `varchar(${s.anno.max})`;
|
|
107
|
+
return "text";
|
|
108
|
+
}
|
|
109
|
+
if (s.type === "boolean") {
|
|
110
|
+
if (s.literal) return null;
|
|
111
|
+
return "boolean";
|
|
112
|
+
}
|
|
113
|
+
if (s.type === "number") {
|
|
114
|
+
if (s.literal) return null;
|
|
115
|
+
if (isIntTag(s.tag)) return INT_PG_TYPE[s.tag] ?? "bigint";
|
|
116
|
+
return FLOAT_PG_TYPE[s.tag] ?? "double precision";
|
|
117
|
+
}
|
|
118
|
+
if (s.type === "date") return "timestamptz";
|
|
119
|
+
if (s.type === "binary") return "bytea";
|
|
120
|
+
return null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const unionEnumValues = (u: shapes.ShapeUnion): string[] | null => {
|
|
124
|
+
const variants = Array.from(u.input) as shapes.Shape[];
|
|
125
|
+
if (variants.length === 0) return null;
|
|
126
|
+
const values: string[] = [];
|
|
127
|
+
for (const v of variants) {
|
|
128
|
+
if (v.type !== "string" || v.literal !== true) return null;
|
|
129
|
+
values.push(v.input as string);
|
|
130
|
+
}
|
|
131
|
+
return values;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const renderType = (
|
|
135
|
+
name: string,
|
|
136
|
+
shape: shapes.Shape,
|
|
137
|
+
): R.Result<TypeRendering, string[]> => {
|
|
138
|
+
const errors: string[] = [];
|
|
139
|
+
const prelude: Prelude[] = [];
|
|
140
|
+
const checks: string[] = [];
|
|
141
|
+
let pgType: string | null = null;
|
|
142
|
+
|
|
143
|
+
if (shape.type === "union") {
|
|
144
|
+
const values = unionEnumValues(shape);
|
|
145
|
+
if (values !== null) {
|
|
146
|
+
const enumName = shape.anno.title;
|
|
147
|
+
if (enumName !== undefined) {
|
|
148
|
+
const sql = `CREATE TYPE ${quoteIdent(enumName)} AS ENUM (${values
|
|
149
|
+
.map(quoteLit)
|
|
150
|
+
.join(", ")});`;
|
|
151
|
+
prelude.push({ name: enumName, sql });
|
|
152
|
+
pgType = quoteIdent(enumName);
|
|
153
|
+
} else {
|
|
154
|
+
pgType = "text";
|
|
155
|
+
checks.push(
|
|
156
|
+
`${quoteIdent(name)} IN (${values.map(quoteLit).join(", ")})`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
pgType = "jsonb";
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
switch (shape.type) {
|
|
164
|
+
case "string": {
|
|
165
|
+
if (shape.literal) {
|
|
166
|
+
pgType = "text";
|
|
167
|
+
checks.push(
|
|
168
|
+
`${quoteIdent(name)} = ${quoteLit(shape.input as string)}`,
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
if (shape.anno.max !== undefined) {
|
|
172
|
+
pgType = `varchar(${shape.anno.max})`;
|
|
173
|
+
} else {
|
|
174
|
+
pgType = "text";
|
|
175
|
+
}
|
|
176
|
+
if (shape.anno.min !== undefined) {
|
|
177
|
+
checks.push(
|
|
178
|
+
`char_length(${quoteIdent(name)}) >= ${shape.anno.min}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (shape.anno.pattern !== undefined) {
|
|
182
|
+
const src =
|
|
183
|
+
shape.anno.pattern instanceof RegExp ?
|
|
184
|
+
shape.anno.pattern.source
|
|
185
|
+
: shape.anno.pattern;
|
|
186
|
+
checks.push(`${quoteIdent(name)} ~ ${quoteLit(src)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case "boolean": {
|
|
192
|
+
pgType = "boolean";
|
|
193
|
+
if (shape.literal) {
|
|
194
|
+
const v = shape.input as boolean;
|
|
195
|
+
checks.push(`${quoteIdent(name)} IS ${v ? "TRUE" : "FALSE"}`);
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case "number": {
|
|
200
|
+
if (shape.literal) {
|
|
201
|
+
if (isIntTag(shape.tag)) {
|
|
202
|
+
pgType = INT_PG_TYPE[shape.tag] ?? "bigint";
|
|
203
|
+
} else {
|
|
204
|
+
pgType = FLOAT_PG_TYPE[shape.tag] ?? "double precision";
|
|
205
|
+
}
|
|
206
|
+
checks.push(
|
|
207
|
+
`${quoteIdent(name)} = ${formatNumberLit(shape.input as number)}`,
|
|
208
|
+
);
|
|
209
|
+
} else if (isIntTag(shape.tag)) {
|
|
210
|
+
pgType = INT_PG_TYPE[shape.tag] ?? "bigint";
|
|
211
|
+
const natural = INT_RANGES[shape.tag];
|
|
212
|
+
let effMin: number | undefined = natural?.minimum;
|
|
213
|
+
let effMax: number | undefined = natural?.maximum;
|
|
214
|
+
if (shape.anno.min !== undefined) {
|
|
215
|
+
effMin =
|
|
216
|
+
effMin === undefined ?
|
|
217
|
+
shape.anno.min
|
|
218
|
+
: Math.max(effMin, shape.anno.min);
|
|
219
|
+
}
|
|
220
|
+
if (shape.anno.max !== undefined) {
|
|
221
|
+
effMax =
|
|
222
|
+
effMax === undefined ?
|
|
223
|
+
shape.anno.max
|
|
224
|
+
: Math.min(effMax, shape.anno.max);
|
|
225
|
+
}
|
|
226
|
+
const pgr = pgTypeRange(pgType);
|
|
227
|
+
if (
|
|
228
|
+
effMin !== undefined &&
|
|
229
|
+
(pgr.minimum === undefined || effMin > pgr.minimum)
|
|
230
|
+
) {
|
|
231
|
+
checks.push(`${quoteIdent(name)} >= ${effMin}`);
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
effMax !== undefined &&
|
|
235
|
+
(pgr.maximum === undefined || effMax < pgr.maximum)
|
|
236
|
+
) {
|
|
237
|
+
checks.push(`${quoteIdent(name)} <= ${effMax}`);
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
pgType = FLOAT_PG_TYPE[shape.tag] ?? "double precision";
|
|
241
|
+
if (shape.anno.min !== undefined)
|
|
242
|
+
checks.push(`${quoteIdent(name)} >= ${shape.anno.min}`);
|
|
243
|
+
if (shape.anno.max !== undefined)
|
|
244
|
+
checks.push(`${quoteIdent(name)} <= ${shape.anno.max}`);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case "date":
|
|
249
|
+
pgType = "timestamptz";
|
|
250
|
+
break;
|
|
251
|
+
case "binary":
|
|
252
|
+
pgType = "bytea";
|
|
253
|
+
break;
|
|
254
|
+
case "unknown":
|
|
255
|
+
pgType = "jsonb";
|
|
256
|
+
break;
|
|
257
|
+
case "nil":
|
|
258
|
+
errors.push(`field '${name}': nil cannot be a column`);
|
|
259
|
+
break;
|
|
260
|
+
case "undefined":
|
|
261
|
+
errors.push(`field '${name}': undefined cannot be a column`);
|
|
262
|
+
break;
|
|
263
|
+
case "array": {
|
|
264
|
+
const elt = arrayElementPgType(shape.input);
|
|
265
|
+
pgType = elt !== null ? `${elt}[]` : "jsonb";
|
|
266
|
+
if (shape.anno.min !== undefined)
|
|
267
|
+
checks.push(`cardinality(${quoteIdent(name)}) >= ${shape.anno.min}`);
|
|
268
|
+
if (shape.anno.max !== undefined)
|
|
269
|
+
checks.push(`cardinality(${quoteIdent(name)}) <= ${shape.anno.max}`);
|
|
270
|
+
if (shape.anno.uniqueItems === true) {
|
|
271
|
+
if (pgType === "jsonb") {
|
|
272
|
+
errors.push(
|
|
273
|
+
`field '${name}': uniqueItems is not supported on jsonb array columns`,
|
|
274
|
+
);
|
|
275
|
+
} else {
|
|
276
|
+
prelude.push({
|
|
277
|
+
name: ARRAY_IS_UNIQUE_FN,
|
|
278
|
+
sql: ARRAY_IS_UNIQUE_FN_SQL,
|
|
279
|
+
});
|
|
280
|
+
checks.push(`${ARRAY_IS_UNIQUE_FN}(${quoteIdent(name)})`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case "vector":
|
|
286
|
+
pgType = `vector(${shape.input.dims})`;
|
|
287
|
+
break;
|
|
288
|
+
case "range": {
|
|
289
|
+
pgType = rangePgType(shape);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case "mapping":
|
|
293
|
+
case "record":
|
|
294
|
+
pgType = "jsonb";
|
|
295
|
+
break;
|
|
296
|
+
case "tuple": {
|
|
297
|
+
pgType = "jsonb";
|
|
298
|
+
const len = shape.input.length;
|
|
299
|
+
checks.push(
|
|
300
|
+
`jsonb_typeof(${quoteIdent(name)}) = 'array' AND jsonb_array_length(${quoteIdent(name)}) = ${len}`,
|
|
301
|
+
);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (pgType === null) {
|
|
308
|
+
return R.err(errors);
|
|
309
|
+
}
|
|
310
|
+
if (errors.length > 0) return R.err(errors);
|
|
311
|
+
|
|
312
|
+
return R.ok({ type: pgType, checks, prelude });
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const formatDefault = (v: unknown, columnType: string): FormatDefault => {
|
|
316
|
+
if (v === null) return { ok: true, expr: "NULL" };
|
|
317
|
+
if (typeof v === "string") return { ok: true, expr: quoteLit(v) };
|
|
318
|
+
if (typeof v === "number") return { ok: true, expr: formatNumberLit(v) };
|
|
319
|
+
if (typeof v === "boolean") return { ok: true, expr: v ? "TRUE" : "FALSE" };
|
|
320
|
+
if (v instanceof Date)
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
expr: `${quoteLit(v.toISOString())}::timestamptz`,
|
|
324
|
+
};
|
|
325
|
+
if (typeof v === "object" && columnType === "jsonb") {
|
|
326
|
+
try {
|
|
327
|
+
return { ok: true, expr: `${quoteLit(JSON.stringify(v))}::jsonb` };
|
|
328
|
+
} catch {
|
|
329
|
+
return { ok: false, reason: "default value could not be JSON-encoded" };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
ok: false,
|
|
334
|
+
reason: `default value of type ${typeof v} is not supported for column type ${columnType}`,
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const applyAutoIncrement = (
|
|
339
|
+
name: string,
|
|
340
|
+
shape: shapes.Shape,
|
|
341
|
+
col: ColumnDef,
|
|
342
|
+
): string[] => {
|
|
343
|
+
if (shape.type !== "number" || !isIntTag(shape.tag)) {
|
|
344
|
+
return [`field '${name}': autoIncrement requires an integer type`];
|
|
345
|
+
}
|
|
346
|
+
if (shape.tag === "uint64") {
|
|
347
|
+
return [
|
|
348
|
+
`field '${name}': autoIncrement is not supported on uint64 (numeric column)`,
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
col.identity = true;
|
|
352
|
+
return [];
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const renderColumn = (c: ColumnDef): string => {
|
|
356
|
+
const parts: string[] = [quoteIdent(c.name), c.type];
|
|
357
|
+
if (c.generated !== undefined) {
|
|
358
|
+
parts.push(
|
|
359
|
+
`GENERATED ALWAYS AS (${c.generated.expression}) ${c.generated.stored ? "STORED" : "VIRTUAL"}`,
|
|
360
|
+
);
|
|
361
|
+
if (c.primary) parts.push("PRIMARY KEY");
|
|
362
|
+
if (c.unique) parts.push("UNIQUE");
|
|
363
|
+
if (c.references !== undefined) parts.push(`REFERENCES ${c.references}`);
|
|
364
|
+
for (const chk of c.checks) parts.push(`CHECK (${chk})`);
|
|
365
|
+
return parts.join(" ");
|
|
366
|
+
}
|
|
367
|
+
if (c.notNull) parts.push("NOT NULL");
|
|
368
|
+
if (c.primary) parts.push("PRIMARY KEY");
|
|
369
|
+
if (c.unique) parts.push("UNIQUE");
|
|
370
|
+
if (c.identity) parts.push("GENERATED ALWAYS AS IDENTITY");
|
|
371
|
+
if (c.defaultExpr !== undefined) parts.push(`DEFAULT ${c.defaultExpr}`);
|
|
372
|
+
if (c.references !== undefined) parts.push(`REFERENCES ${c.references}`);
|
|
373
|
+
for (const chk of c.checks) parts.push(`CHECK (${chk})`);
|
|
374
|
+
return parts.join(" ");
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export const postgresDialect: Dialect = {
|
|
378
|
+
name: "postgres",
|
|
379
|
+
supportsSchema: true,
|
|
380
|
+
tableTrailer: "",
|
|
381
|
+
renderType,
|
|
382
|
+
applyAutoIncrement,
|
|
383
|
+
formatDefault,
|
|
384
|
+
validateMappingAnno: (_anno, _title) => [],
|
|
385
|
+
schemaPrelude: (name) => `CREATE SCHEMA IF NOT EXISTS ${quoteIdent(name)};`,
|
|
386
|
+
qualifiedTable: (schema, title) =>
|
|
387
|
+
schema !== undefined ?
|
|
388
|
+
`${quoteIdent(schema)}.${quoteIdent(title)}`
|
|
389
|
+
: quoteIdent(title),
|
|
390
|
+
renderColumn,
|
|
391
|
+
finalizeColumns: () => [],
|
|
392
|
+
};
|