@@ -2,10 +2,11 @@ import type { Column, Constraint, Index, Relation, Schema } from "./types.ts";
22import DB from "./db.ts" ;
33
44const dataTypes = {
5+ array : "JSON" ,
56 boolean : "BOOLEAN" ,
67 date : "DATETIME" ,
78 integer : "INTEGER" ,
8- json : "JSON" ,
9+ object : "JSON" ,
910 number : "DOUBLE" ,
1011 string : "VARCHAR" ,
1112} ;
@@ -17,22 +18,48 @@ const serialType = {
1718} ;
1819
1920const _BaseSchema : DB . Schema = {
20- name : "_BaseSchema" ,
21+ table : "_BaseSchema" ,
2122 properties : {
22- id : { type : "integer" , required : true , primaryKey : true , description : "Unique identifier, auto-generated. It's the primary key." } ,
23- insertedAt : { type : "date" , required : false , dateOn : "insert" , description : "Timestamp when current record is inserted" } ,
24- updatedAt : { type : "date" , required : false , dateOn : "update" , description : "Timestamp when current record is updated" } ,
25- etag : { type : "string" , required : false , maxLength : 1024 , description : "Possible ETag for all resources that are external. Allows for better synch-ing." } ,
23+ id : { type : "integer" , primaryKey : true , description : "Unique identifier, auto-generated. It's the primary key." } ,
24+ insertedAt : { type : "date" , dateOn : "insert" , description : "Timestamp when current record is inserted" } ,
25+ updatedAt : { type : "date" , dateOn : "update" , description : "Timestamp when current record is updated" } ,
26+ etag : { type : "string" , maxLength : 1024 , description : "Possible ETag for all resources that are external. Allows for better synch-ing." } ,
2627 } ,
2728 indices : [
28- { name : "insertedAt" , properties : [ "insertedAt" ] } ,
29- { name : "updatedAt" , properties : [ "updatedAt" ] } ,
29+ { properties : [ "insertedAt" ] } ,
30+ { properties : [ "updatedAt" ] } ,
3031 ] ,
3132} ;
3233
3334export class DDL {
35+ static EXTENSIONS = [ "as" , "constraints" , "dateOn" , "fullText" , "index" , "primaryKey" , "relations" , "table" ] ;
36+
3437 static padWidth = 4 ;
35- static defaultWidth = 256 ;
38+ static defaultWidth = 128 ;
39+ static textWidth = 128 ;
40+
41+ /**
42+ * When using tools such as [TJS](https://github.com/YousefED/typescript-json-schema) to
43+ * generate JSON schemas from TypeScript classes, the resulting schema may need some
44+ * cleaning up. This function does that.
45+ *
46+ * @param schema - the tool-generated schema
47+ * @param type - the type of the schema which we may need to correct/override
48+ */
49+ static cleanSchema ( schema : Schema , type ?: string , table ?: string ) {
50+ if ( type ) schema . type = type ;
51+ if ( table ) schema . table = table ;
52+ if ( typeof ( schema . fullText ) === "string" ) schema . fullText = ( schema . fullText as string ) . split ( "," ) . map ( s => s . trim ( ) ) ;
53+ schema . indices ??= [ ] ;
54+ Object . values ( schema . properties ) . forEach ( ( c ) => {
55+ if ( ! c . type ) c . type = "string" ;
56+ if ( typeof c . primaryKey === "string" ) c . primaryKey = true ;
57+ if ( typeof c . uniqueItems === "string" ) c . uniqueItems = true ;
58+ if ( c . format === "date-time" ) c . type = "date" ;
59+ if ( typeof c . index === "string" ) schema . indices ! . push ( { properties : ( c . index as string ) . split ( "," ) . map ( s => s . trim ( ) ) } ) ;
60+ } ) ;
61+ return schema ;
62+ }
3663
3764 // Enhance schema with standard properties
3865 static enhanceSchema ( schema : Schema , selected : string [ ] = [ "id" , "insertedAt" , "updatedAt" ] ) : Schema {
@@ -42,29 +69,30 @@ export class DDL {
4269
4370 // Select indices
4471 if ( ! schema . indices ) schema . indices = [ ] ;
45- for ( const name of selected ) {
46- const index = _BaseSchema . indices ?. find ( ( i ) => i . name === name ) ;
47- if ( index ) schema . indices . push ( index ) ;
48- }
72+ if ( selected . includes ( "insertedAt" ) ) schema . indices . push ( { properties : [ "insertedAt" ] } ) ;
73+ if ( selected . includes ( "updatedAt" ) ) schema . indices . push ( { properties : [ "updatedAt" ] } ) ;
4974
5075 return schema ;
5176 }
5277
5378 // Column generator
54- static createColumn ( dbType : string , name : string , column : Column , namePad : number , padWidth = DDL . padWidth , defaultWidth = DDL . defaultWidth ) : string {
79+ static createColumn ( dbType : string , name : string , column : Column , required : boolean , namePad : number , padWidth = DDL . padWidth , defaultWidth = DDL . defaultWidth ) : string {
80+ if ( typeof ( column . default ) === "string" && ! column . default . startsWith ( "'" ) && ! column . default . endsWith ( "'" ) ) column . default = "'" + column . default + "'" ;
5581 if ( typeof ( column . default ) === "object" ) column . default = "('" + JSON . stringify ( column . default ) + "')" ;
5682 if ( column . dateOn === "insert" ) column . default = "CURRENT_TIMESTAMP" ;
5783 if ( column . dateOn === "update" ) column . default = "CURRENT_TIMESTAMP" + ( ( dbType !== DB . Provider . MYSQL ) ? "" : " ON UPDATE CURRENT_TIMESTAMP" ) ;
5884 const pad = "" . padEnd ( padWidth ) ;
5985 let type = dataTypes [ column . type as keyof typeof dataTypes ] ;
60- const autoIncrement = column . primaryKey && column . type === "integer" ;
61- const length = column . maxLength || type . endsWith ( "CHAR" ) ? "(" + ( column . maxLength ?? defaultWidth ) + ")" : "" ;
62- const nullable = column . primaryKey || column . required ? " NOT NULL" : "" ;
86+ if ( column . maxLength ! > this . textWidth ) type = "TEXT" ;
87+ const primaryKey = ( column . primaryKey !== undefined ) ;
88+ const autoIncrement = primaryKey && column . type === "integer" ;
89+ const length = column . maxLength ! < this . textWidth || type . endsWith ( "CHAR" ) ? "(" + ( column . maxLength ?? defaultWidth ) + ")" : "" ;
90+ const nullable = primaryKey || required ? " NOT NULL" : "" ;
6391 const gen = autoIncrement ? serialType [ dbType as keyof typeof serialType ] : "" ;
64- const asExpression = column . asExpression && ( typeof column . asExpression === "string" ? DB . _sqlFilter ( column . asExpression ) : column . asExpression [ dbType ] ) ;
65- const as = asExpression ? " GENERATED ALWAYS AS (" + asExpression + ") " + ( column . generatedType ?. toUpperCase ( ) || "VIRTUAL" ) : "" ;
92+ const expr = column . as && ( typeof column . as === "string" ? DB . _sqlFilter ( column . as ) : column . as [ dbType ] ) ;
93+ const as = expr ? " GENERATED ALWAYS AS (" + expr + ") STORED" : "" ;
6694 const def = Object . hasOwn ( column , "default" ) ? " DEFAULT " + column . default : "" ;
67- const key = column . primaryKey ? " PRIMARY KEY" : ( column . unique ? " UNIQUE" : "" ) ;
95+ const key = primaryKey ? " PRIMARY KEY" : ( column . uniqueItems !== undefined ? " UNIQUE" : "" ) ;
6896 const comment = ( dbType === DB . Provider . MYSQL ) && column . description ? " COMMENT '" + column . description . replace ( / ' / g, "''" ) + "'" : "" ;
6997
7098 // Correct Postgres JSON type
@@ -75,19 +103,19 @@ export class DDL {
75103 }
76104
77105 // Index generator
78- static createIndex ( dbType : string , indice : Index , padWidth = 4 , table : string ) : string {
106+ static createIndex ( dbType : string , index : Index , padWidth = 4 , table : string ) : string {
79107 const pad = "" . padEnd ( padWidth ) ;
80- const columns = [ ...indice . properties ] as string [ ] ;
108+ const columns = [ ...index . properties ] as string [ ] ;
109+ const name = columns . join ( "_" ) ;
81110
82111 // If there is an array expression, replace the column by it
83112 // TODO: multivalued indexes only supported on MYSQL for now, Postgres and SQLite will use the entire
84- const subType = indice . subType ?? "CHAR(32)" ;
85- if ( indice . array !== undefined ) {
86- columns [ indice . array ] = "(CAST(" + columns [ indice . array ] + " AS " + subType + ( dbType === DB . Provider . MYSQL ? " ARRAY" : "" ) + "))" ;
113+ const subType = index . subType ?? "CHAR(32)" ;
114+ if ( index . array !== undefined ) {
115+ columns [ index . array ] = "(CAST(" + columns [ index . array ] + " AS " + subType + ( dbType === DB . Provider . MYSQL ? " ARRAY" : "" ) + "))" ;
87116 }
88117
89- const name = indice . name ?? "" ;
90- const unique = indice . unique ? "UNIQUE " : "" ;
118+ const unique = index . unique ? "UNIQUE " : "" ;
91119 return `${ pad } CREATE ${ unique } INDEX ${ table } _${ name } ON ${ table } (${ columns . join ( "," ) } );\n` ;
92120 }
93121
@@ -138,14 +166,15 @@ export class DDL {
138166 const sqlite = dbType === DB . Provider . SQLITE ;
139167
140168 // Create SQL
141- const table = nameOverride ?? schema . name ;
142- const columns = Object . entries ( schema . properties ) . map ( ( [ n , c ] ) => this . createColumn ( dbType , n , c ! , namePad ) ) . join ( "" ) ;
143- const relations = ! sqlite && Object . entries ( schema . relations || [ ] ) . map ( ( [ n , r ] ) => this . createRelation ( dbType , schema . name , n , r ! ) ) . join ( "" ) || "" ;
169+ const table = nameOverride ?? schema . table ! ?? schema . type ?. toLowerCase ( ) ;
170+ const required = ( n : string ) => schema . required ?. includes ( n ) || false ;
171+ const columns = Object . entries ( schema . properties ) . map ( ( [ n , c ] ) => this . createColumn ( dbType , n , c ! , required ( n ) , namePad ) ) . join ( "" ) ;
172+ const relations = ! sqlite && Object . entries ( schema . relations || [ ] ) . map ( ( [ n , r ] ) => this . createRelation ( dbType , table , n , r ! ) ) . join ( "" ) || "" ;
144173
145174 // Create constraints
146175 const filter = ( c : Constraint ) => ! c . provider || c . provider === dbType ;
147- const columnConstraints = Object . entries ( schema . properties || { } ) . map ( ( [ n , c ] ) => this . createColumnConstraint ( dbType , schema . name , n , c ) ) ;
148- const independentConstraints = ( schema . constraints || [ ] ) . filter ( filter ) . map ( ( c ) => this . createIndependentConstraint ( dbType , schema . name , c ) ) ;
176+ const columnConstraints = Object . entries ( schema . properties || { } ) . map ( ( [ n , c ] ) => this . createColumnConstraint ( dbType , table , n , c ) ) ;
177+ const independentConstraints = ( schema . constraints || [ ] ) . filter ( filter ) . map ( ( c ) => this . createIndependentConstraint ( dbType , table , c ) ) ;
149178 const constraints = ! sqlite && [ ...columnConstraints , ...independentConstraints ] . join ( "" ) || "" ;
150179
151180 // Create sql
@@ -155,8 +184,7 @@ export class DDL {
155184 if ( schema . indices ) sql += "\n" + schema . indices ?. map ( ( i ) => this . createIndex ( dbType , i , 0 , table ) ) . join ( "" ) ;
156185
157186 // Full text index
158- const fullTextColumns = Object . entries ( schema . properties ) . filter ( ( [ _ , c ] ) => c . fullText ) . map ( ( [ n , _ ] ) => n ) ;
159- if ( fullTextColumns . length ) sql += this . createFullTextIndex ( dbType , fullTextColumns , 0 , table ) ;
187+ if ( schema . fullText ?. length ) sql += this . createFullTextIndex ( dbType , schema . fullText , 0 , table ) ;
160188
161189 const fixDanglingComma = ( sql : string ) => sql . replace ( / , \n \) / , "\n);" ) ;
162190 if ( dbType === DB . Provider . POSTGRES ) sql = this . #postgres( sql ) ;
0 commit comments