diff --git a/packages/dbml-cli/src/cli/config.js b/packages/dbml-cli/src/cli/config.js index 2bfe5b059..0426ae2e4 100644 --- a/packages/dbml-cli/src/cli/config.js +++ b/packages/dbml-cli/src/cli/config.js @@ -20,4 +20,7 @@ export default { snowflake: { name: 'Snowflake', }, + sqlite: { + name: 'SQLite' + } }; diff --git a/packages/dbml-cli/src/cli/index.js b/packages/dbml-cli/src/cli/index.js index 0574ca294..1706bbf6d 100644 --- a/packages/dbml-cli/src/cli/index.js +++ b/packages/dbml-cli/src/cli/index.js @@ -18,6 +18,7 @@ function dbml2sql (args) { .option('--postgres') .option('--mssql') .option('--oracle') + .option('--sqlite') .option('-o, --out-file ', 'compile all input files into a single files'); // .option('-d, --out-dir ', 'compile an input directory of dbml files into an output directory'); diff --git a/packages/dbml-cli/src/cli/utils.js b/packages/dbml-cli/src/cli/utils.js index 1726c82a4..d0ea88de3 100644 --- a/packages/dbml-cli/src/cli/utils.js +++ b/packages/dbml-cli/src/cli/utils.js @@ -16,7 +16,7 @@ function validateInputFilePaths (paths, validatePlugin) { function getFormatOpt (opts) { const formatOpts = Object.keys(opts).filter((opt) => { - return ['postgres', 'mysql', 'mssql', 'postgresLegacy', 'mysqlLegacy', 'mssqlLegacy', 'oracle', 'snowflake'].includes(opt); + return ['postgres', 'mysql', 'mssql', 'postgresLegacy', 'mysqlLegacy', 'mssqlLegacy', 'oracle', 'snowflake', 'sqlite'].includes(opt); }); let format = 'postgres'; diff --git a/packages/dbml-core/__tests__/exporter/exporter.spec.js b/packages/dbml-core/__tests__/exporter/exporter.spec.js index 6962254d2..b412e71fe 100644 --- a/packages/dbml-core/__tests__/exporter/exporter.spec.js +++ b/packages/dbml-core/__tests__/exporter/exporter.spec.js @@ -31,5 +31,10 @@ describe('@dbml/core - exporter', () => { test.each(scanTestNames(__dirname, 'oracle_exporter/input'))('oracle_exporter/%s', (name) => { runTest(name, 'oracle_exporter', 'oracle'); }); + + test.each(scanTestNames(__dirname, 'sqlite_exporter/input'))('sqlite_exporter/%s', (name) => { + runTest(name, 'sqlite_exporter', 'sqlite'); + }); + /* eslint-enable */ }); diff --git a/packages/dbml-core/__tests__/exporter/sqlite_exporter/input/1_to_1_relations.in.dbml b/packages/dbml-core/__tests__/exporter/sqlite_exporter/input/1_to_1_relations.in.dbml new file mode 100644 index 000000000..9658a9fd8 --- /dev/null +++ b/packages/dbml-core/__tests__/exporter/sqlite_exporter/input/1_to_1_relations.in.dbml @@ -0,0 +1,8 @@ +Table father { + obj_id char(50) [primary key, unique] +} + +Table child { + obj_id char(50) [primary key, unique] + father_obj_id char(50) [ref: - father.obj_id] +} \ No newline at end of file diff --git a/packages/dbml-core/__tests__/exporter/sqlite_exporter/input/general_schema.in.dbml b/packages/dbml-core/__tests__/exporter/sqlite_exporter/input/general_schema.in.dbml new file mode 100644 index 000000000..86fe52493 --- /dev/null +++ b/packages/dbml-core/__tests__/exporter/sqlite_exporter/input/general_schema.in.dbml @@ -0,0 +1,81 @@ +Enum "orders_status_enum" { + "created" + "running" + "done" + "failure" +} + +Enum "products_status_enum" { + "Out of Stock" + "In Stock" +} + +Table "orders" { + "id" int [pk, increment] + "user_id" int [unique, not null] + "status" orders_status_enum + "created_at" varchar(255) [note: 'When order created'] + + Note: 'This is a note in table "orders"' +} + +Table "order_items" { + "order_id" int + "product_id" int + "quantity" int [default: 1] +} + +Table "products" { + "id" int + "name" varchar(255) + "merchant_id" int [not null] + "price" int + "status" products_status_enum + "created_at" datetime [default: `CURRENT_TIMESTAMP`] + + Indexes { + (id, name) [pk] + (merchant_id, status) [name: "product_status"] + id [type: hash, unique, name: "products_index_1"] + } + + Note: "This is a note in table 'products'" +} + +Table "users" { + "id" int [pk] + "full_name" varchar(255) + "email" varchar(255) [unique] + "gender" varchar(255) + "date_of_birth" varchar(255) + "created_at" varchar(255) + "country_code" int + + Note: 'This is a note in table "users"' +} + +Table "merchants" { + "id" int [pk] + "merchant_name" varchar(255) + "country_code" int + "created_at" varchar(255) + "admin_id" int +} + +Table "countries" { + "code" int [pk] + "name" varchar(255) + "continent_name" varchar(255) +} + +Ref:"orders"."id" < "order_items"."order_id" + +Ref:"products"."id" < "order_items"."product_id" + +Ref:"countries"."code" < "users"."country_code" + +Ref:"countries"."code" < "merchants"."country_code" + +Ref:"merchants"."id" < "products"."merchant_id" + +Ref:"users"."id" < "merchants"."admin_id" diff --git a/packages/dbml-core/__tests__/exporter/sqlite_exporter/input/many_to_many_relationship.in.dbml b/packages/dbml-core/__tests__/exporter/sqlite_exporter/input/many_to_many_relationship.in.dbml new file mode 100644 index 000000000..6ffc99457 --- /dev/null +++ b/packages/dbml-core/__tests__/exporter/sqlite_exporter/input/many_to_many_relationship.in.dbml @@ -0,0 +1,86 @@ +table "A"."a" { + "AB" integer [pk] + "BA" integer [pk] +} + +table "B"."b" { + "BC" integer [pk] + "CB" integer [pk] +} + +table "C"."c" { + "CD" integer [pk, ref: <> "D"."d"."DE"] + "DC" integer +} + +table "D"."d" { + "DE" integer [pk] + "ED" integer +} + +table "E"."e" { + "EF" integer [pk] + "FE" integer [pk] + "DE" integer + "ED" integer +} + +table "G"."g" { + "GH" integer [pk] + "HG" integer [pk] + "EH" integer + "HE" integer +} + +ref: "A"."a".("AB","BA") <> "B"."b".("BC","CB") +ref: "E"."e".("EF","FE") <> "G"."g".("GH","HG") + + +table t1 { + a int [pk] + b int [unique] +} + +table t2 { + a int [pk] + b int [unique] +} + +table t1_t2 { + a int +} + +ref: t1.a <> t2.a +ref: t1.b <> t2.b + +Table schema.image { + id integer [pk] + url varchar +} + +Table schema.content_item { + id integer [pk] + heading varchar + description varchar +} + +Ref: schema.image.id <> schema.content_item.id + +Table schema.footer_item { + id integer [pk] + left varchar + centre varchar + right varchar +} + +Table "schema1"."customers" { + "id" integer [pk] + "full_name" varchar +} + +Table "schema2"."orders" { + "id" integer [pk] + "total_price" integer +} + +Ref: "schema1"."customers"."id" <> "schema2"."orders"."id" diff --git a/packages/dbml-core/__tests__/exporter/sqlite_exporter/output/1_to_1_relations.out.sql b/packages/dbml-core/__tests__/exporter/sqlite_exporter/output/1_to_1_relations.out.sql new file mode 100644 index 000000000..a87553a9f --- /dev/null +++ b/packages/dbml-core/__tests__/exporter/sqlite_exporter/output/1_to_1_relations.out.sql @@ -0,0 +1,11 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE "father" ( + "obj_id" TEXT UNIQUE PRIMARY KEY +); + +CREATE TABLE "child" ( + "obj_id" TEXT UNIQUE PRIMARY KEY, + "father_obj_id" TEXT, + FOREIGN KEY ("father_obj_id") REFERENCES "father" ("obj_id") +); diff --git a/packages/dbml-core/__tests__/exporter/sqlite_exporter/output/general_schema.out.sql b/packages/dbml-core/__tests__/exporter/sqlite_exporter/output/general_schema.out.sql new file mode 100644 index 000000000..6d26a59c6 --- /dev/null +++ b/packages/dbml-core/__tests__/exporter/sqlite_exporter/output/general_schema.out.sql @@ -0,0 +1,58 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE "orders" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "user_id" INTEGER UNIQUE NOT NULL, + "status" TEXT CHECK ("status" IN ('created','running','done','failure')), + "created_at" TEXT +); + +CREATE TABLE "countries" ( + "code" INTEGER PRIMARY KEY, + "name" TEXT, + "continent_name" TEXT +); + +CREATE TABLE "users" ( + "id" INTEGER PRIMARY KEY, + "full_name" TEXT, + "email" TEXT UNIQUE, + "gender" TEXT, + "date_of_birth" TEXT, + "created_at" TEXT, + "country_code" INTEGER, + FOREIGN KEY ("country_code") REFERENCES "countries" ("code") +); + +CREATE TABLE "merchants" ( + "id" INTEGER PRIMARY KEY, + "merchant_name" TEXT, + "country_code" INTEGER, + "created_at" TEXT, + "admin_id" INTEGER, + FOREIGN KEY ("country_code") REFERENCES "countries" ("code"), + FOREIGN KEY ("admin_id") REFERENCES "users" ("id") +); + +CREATE TABLE "products" ( + "id" INTEGER, + "name" TEXT, + "merchant_id" INTEGER NOT NULL, + "price" INTEGER, + "status" TEXT CHECK ("status" IN ('Out of Stock','In Stock')), + "created_at" TEXT DEFAULT (CURRENT_TIMESTAMP), + PRIMARY KEY ("id","name"), + FOREIGN KEY ("merchant_id") REFERENCES "merchants" ("id") +); + +CREATE TABLE "order_items" ( + "order_id" INTEGER, + "product_id" INTEGER, + "quantity" INTEGER DEFAULT 1, + FOREIGN KEY ("order_id") REFERENCES "orders" ("id"), + FOREIGN KEY ("product_id") REFERENCES "products" ("id") +); + +CREATE INDEX "product_status" ON "products" ("merchant_id","status"); + +CREATE UNIQUE INDEX "products_index_1" ON "products" ("id"); diff --git a/packages/dbml-core/__tests__/exporter/sqlite_exporter/output/many_to_many_relationship.out.sql b/packages/dbml-core/__tests__/exporter/sqlite_exporter/output/many_to_many_relationship.out.sql new file mode 100644 index 000000000..5b5eb18d1 --- /dev/null +++ b/packages/dbml-core/__tests__/exporter/sqlite_exporter/output/many_to_many_relationship.out.sql @@ -0,0 +1,141 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE "a" ( + "AB" INTEGER, + "BA" INTEGER, + PRIMARY KEY ("AB","BA") +); + +CREATE TABLE "b" ( + "BC" INTEGER, + "CB" INTEGER, + PRIMARY KEY ("BC","CB") +); + +CREATE TABLE "c" ( + "CD" INTEGER PRIMARY KEY, + "DC" INTEGER +); + +CREATE TABLE "d" ( + "DE" INTEGER PRIMARY KEY, + "ED" INTEGER +); + +CREATE TABLE "e" ( + "EF" INTEGER, + "FE" INTEGER, + "DE" INTEGER, + "ED" INTEGER, + PRIMARY KEY ("EF","FE") +); + +CREATE TABLE "g" ( + "GH" INTEGER, + "HG" INTEGER, + "EH" INTEGER, + "HE" INTEGER, + PRIMARY KEY ("GH","HG") +); + +CREATE TABLE "t1" ( + "a" INTEGER PRIMARY KEY, + "b" INTEGER UNIQUE +); + +CREATE TABLE "t2" ( + "a" INTEGER PRIMARY KEY, + "b" INTEGER UNIQUE +); + +CREATE TABLE "t1_t2" ( + "a" INTEGER +); + +CREATE TABLE "image" ( + "id" INTEGER PRIMARY KEY, + "url" TEXT +); + +CREATE TABLE "content_item" ( + "id" INTEGER PRIMARY KEY, + "heading" TEXT, + "description" TEXT +); + +CREATE TABLE "footer_item" ( + "id" INTEGER PRIMARY KEY, + "left" TEXT, + "centre" TEXT, + "right" TEXT +); + +CREATE TABLE "customers" ( + "id" INTEGER PRIMARY KEY, + "full_name" TEXT +); + +CREATE TABLE "orders" ( + "id" INTEGER PRIMARY KEY, + "total_price" INTEGER +); + +CREATE TABLE "d_c" ( + "d_DE" INTEGER, + "c_CD" INTEGER, + PRIMARY KEY ("d_DE","c_CD"), + FOREIGN KEY ("d_DE") REFERENCES "d" ("DE"), + FOREIGN KEY ("c_CD") REFERENCES "c" ("CD") +); + +CREATE TABLE "a_b" ( + "a_AB" INTEGER, + "a_BA" INTEGER, + "b_BC" INTEGER, + "b_CB" INTEGER, + PRIMARY KEY ("a_AB","a_BA","b_BC","b_CB"), + FOREIGN KEY ("a_AB","a_BA") REFERENCES "a" ("AB","BA"), + FOREIGN KEY ("b_BC","b_CB") REFERENCES "b" ("BC","CB") +); + +CREATE TABLE "e_g" ( + "e_EF" INTEGER, + "e_FE" INTEGER, + "g_GH" INTEGER, + "g_HG" INTEGER, + PRIMARY KEY ("e_EF","e_FE","g_GH","g_HG"), + FOREIGN KEY ("e_EF","e_FE") REFERENCES "e" ("EF","FE"), + FOREIGN KEY ("g_GH","g_HG") REFERENCES "g" ("GH","HG") +); + +CREATE TABLE "t1_t2(1)" ( + "t1_a" INTEGER, + "t2_a" INTEGER, + PRIMARY KEY ("t1_a","t2_a"), + FOREIGN KEY ("t1_a") REFERENCES "t1" ("a"), + FOREIGN KEY ("t2_a") REFERENCES "t2" ("a") +); + +CREATE TABLE "t1_t2(2)" ( + "t1_b" INTEGER, + "t2_b" INTEGER, + PRIMARY KEY ("t1_b","t2_b"), + FOREIGN KEY ("t1_b") REFERENCES "t1" ("b"), + FOREIGN KEY ("t2_b") REFERENCES "t2" ("b") +); + +CREATE TABLE "image_content_item" ( + "image_id" INTEGER, + "content_item_id" INTEGER, + PRIMARY KEY ("image_id","content_item_id"), + FOREIGN KEY ("image_id") REFERENCES "image" ("id"), + FOREIGN KEY ("content_item_id") REFERENCES "content_item" ("id") +); + +CREATE TABLE "customers_orders" ( + "customers_id" INTEGER, + "orders_id" INTEGER, + PRIMARY KEY ("customers_id","orders_id"), + FOREIGN KEY ("customers_id") REFERENCES "customers" ("id"), + FOREIGN KEY ("orders_id") REFERENCES "orders" ("id") +); diff --git a/packages/dbml-core/jestHelpers.js b/packages/dbml-core/jestHelpers.js index b6bf81000..e7893f05c 100644 --- a/packages/dbml-core/jestHelpers.js +++ b/packages/dbml-core/jestHelpers.js @@ -16,7 +16,7 @@ global.getFileExtension = (format) => { return 'rb'; } - const SQL_FORMATS = ['mysql', 'postgres', 'mssql', 'oracle', 'snowflake']; + const SQL_FORMATS = ['mysql', 'postgres', 'mssql', 'oracle', 'snowflake', 'sqlite']; if (SQL_FORMATS.includes(format)) { return 'sql'; } diff --git a/packages/dbml-core/src/export/ModelExporter.js b/packages/dbml-core/src/export/ModelExporter.js index 4a734db5f..6476a5bcb 100644 --- a/packages/dbml-core/src/export/ModelExporter.js +++ b/packages/dbml-core/src/export/ModelExporter.js @@ -4,6 +4,7 @@ import PostgresExporter from './PostgresExporter'; import JsonExporter from './JsonExporter'; import SqlServerExporter from './SqlServerExporter'; import OracleExporter from './OracleExporter'; +import SqliteExporter from './SqliteExporter'; class ModelExporter { static export (model = {}, format = '', isNormalized = true) { @@ -34,6 +35,10 @@ class ModelExporter { res = OracleExporter.export(normalizedModel); break; + case 'sqlite': + res = SqliteExporter.export(normalizedModel); + break; + default: break; } diff --git a/packages/dbml-core/src/export/SqliteExporter.js b/packages/dbml-core/src/export/SqliteExporter.js new file mode 100644 index 000000000..a477ec412 --- /dev/null +++ b/packages/dbml-core/src/export/SqliteExporter.js @@ -0,0 +1,338 @@ +/* eslint-disable max-len */ +import _ from 'lodash'; +import { + buildJunctionFields1, + buildJunctionFields2, + buildNewTableName, +} from './utils'; +import { shouldPrintSchemaName } from '../model_structure/utils'; + +function escapeSingleQuotes(s) { + return String(s || '').replace(/'/g, "''"); +} + +// Minimal boolean type detection - only used to add CHECK (field IN (0,1)) constraints +// for fields that should be boolean in nature +const BOOLEAN_TYPES = new Set(['BOOLEAN', 'BOOL']); + +function isBooleanType(typeName) { + const normalized = (typeName || '').trim().toUpperCase(); + return BOOLEAN_TYPES.has(normalized); +} + +function topoSortTables(allTableIds, dependencyEdges) { + const parentsByChild = new Map(); + const childrenByParent = new Map(); + const indegree = new Map(); + + allTableIds.forEach(id => { + indegree.set(id, 0); + parentsByChild.set(id, new Set()); + childrenByParent.set(id, new Set()); + }); + + (dependencyEdges || []).forEach(([child, parent]) => { + if (!parentsByChild.has(child)) parentsByChild.set(child, new Set()); + if (!childrenByParent.has(parent)) childrenByParent.set(parent, new Set()); + if (!parentsByChild.get(child).has(parent)) { + parentsByChild.get(child).add(parent); + childrenByParent.get(parent).add(child); + indegree.set(child, (indegree.get(child) || 0) + 1); + } + }); + + const q = []; + indegree.forEach((deg, id) => { if (deg === 0) q.push(id); }); + + const ordered = []; + while (q.length) { + const node = q.shift(); + ordered.push(node); + (childrenByParent.get(node) || []).forEach(child => { + indegree.set(child, indegree.get(child) - 1); + if (indegree.get(child) === 0) q.push(child); + }); + } + + if (ordered.length !== allTableIds.length) { + return allTableIds; + } + return ordered; +} + + +class SqliteExporter { + static buildEnumMap(model) { + const map = new Map(); + Object.keys(model.enums).forEach((enumId) => { + const e = model.enums[enumId]; + const schema = model.schemas[e.schemaId]; + const fq = `"${schema.name}"."${e.name}"`; + const local = `"${e.name}"`; + const vals = e.valueIds.map(id => model.enumValues[id].name); + map.set(fq, vals); + map.set(local, vals); + map.set(e.name, vals); + }); + return map; + } + + // Collect per-table FK clauses (since SQLite needs inline FKs) + static collectForeignKeysByTable(refIds, model) { + const fksByTableId = new Map(); + const junctionCreates = []; + const dependencyEdges = []; + + const usedTableNames = new Set(Object.values(model.tables).map(t => t.name)); + + (refIds || []).forEach((refId) => { + const ref = model.refs[refId]; + const refOneIndex = ref.endpointIds.findIndex(endpointId => model.endpoints[endpointId].relation === '1'); + const refEndpointIndex = refOneIndex === -1 ? 0 : refOneIndex; + const foreignEndpointId = ref.endpointIds[1 - refEndpointIndex]; + const refEndpointId = ref.endpointIds[refEndpointIndex]; + const foreignEndpoint = model.endpoints[foreignEndpointId]; + const refEndpoint = model.endpoints[refEndpointId]; + + const refField = model.fields[refEndpoint.fieldIds[0]]; + const refTable = model.tables[refField.tableId]; + const foreignField = model.fields[foreignEndpoint.fieldIds[0]]; + const foreignTable = model.tables[foreignField.tableId]; + + const refCols = SqliteExporter.buildFieldName(refEndpoint.fieldIds, model); + const foreignCols = SqliteExporter.buildFieldName(foreignEndpoint.fieldIds, model); + + if (refOneIndex === -1) { + const firstTableFieldsMap = buildJunctionFields1(refEndpoint.fieldIds, model); + const secondTableFieldsMap = buildJunctionFields2(foreignEndpoint.fieldIds, model, firstTableFieldsMap); + const newTableName = buildNewTableName(refTable.name, foreignTable.name, usedTableNames); + + let line = `CREATE TABLE "${newTableName}" (\n`; + const key1s = [...firstTableFieldsMap.keys()].join('","'); + const key2s = [...secondTableFieldsMap.keys()].join('","'); + + firstTableFieldsMap.forEach((fieldType, fieldName) => { + line += ` "${fieldName}" ${fieldType},\n`; + }); + secondTableFieldsMap.forEach((fieldType, fieldName) => { + line += ` "${fieldName}" ${fieldType},\n`; + }); + + line += ` PRIMARY KEY ("${key1s}","${key2s}"),\n`; + + const refColsList = [...firstTableFieldsMap.keys()].map(k => `"${k}"`).join(','); + const forColsList = [...secondTableFieldsMap.keys()].map(k => `"${k}"`).join(','); + + line += ` FOREIGN KEY (${refColsList}) REFERENCES "${refTable.name}" ${refCols}`; + if (ref.onDelete) line += ` ON DELETE ${ref.onDelete.toUpperCase()}`; + if (ref.onUpdate) line += ` ON UPDATE ${ref.onUpdate.toUpperCase()}`; + line += ',\n'; + + line += ` FOREIGN KEY (${forColsList}) REFERENCES "${foreignTable.name}" ${foreignCols}`; + if (ref.onDelete) line += ` ON DELETE ${ref.onDelete.toUpperCase()}`; + if (ref.onUpdate) line += ` ON UPDATE ${ref.onUpdate.toUpperCase()}`; + line += '\n);\n'; + + junctionCreates.push(line); + } else { + const fkClauseParts = []; + fkClauseParts.push(`FOREIGN KEY ${foreignCols} REFERENCES "${refTable.name}" ${refCols}`); + if (ref.onDelete) fkClauseParts.push(`ON DELETE ${ref.onDelete.toUpperCase()}`); + if (ref.onUpdate) fkClauseParts.push(`ON UPDATE ${ref.onUpdate.toUpperCase()}`); + + const fkLine = fkClauseParts.join(' '); + const tableId = foreignTable.id; + if (!fksByTableId.has(tableId)) fksByTableId.set(tableId, []); + fksByTableId.get(tableId).push(fkLine); + dependencyEdges.push([foreignTable.id, refTable.id]); + } + }); + + return { fksByTableId, junctionCreates, dependencyEdges }; + } + + static buildFieldName(fieldIds, model) { + const fieldNames = fieldIds.map(fieldId => `"${model.fields[fieldId].name}"`).join(','); + return `(${fieldNames})`; + } + + static getFieldLines(tableId, model, enumMap) { + const table = model.tables[tableId]; + + const lines = table.fieldIds.map((fieldId) => { + const field = model.fields[fieldId]; + + let typeName; + let isBoolean = false; + let enumCheck = ''; + + if (!field.type.schemaName || !shouldPrintSchemaName(field.type.schemaName)) { + const originalTypeName = field.type.type_name; + + const enumKeys = [ + `"${field.type.schemaName}"."${field.type.type_name}"`, + `"${field.type.type_name}"`, + field.type.type_name, + ].filter(Boolean); + + let enumVals = null; + for (const k of enumKeys) { + if (enumMap.has(k)) { enumVals = enumMap.get(k); break; } + } + + if (enumVals && enumVals.length) { + typeName = 'TEXT'; + enumCheck = ` CHECK ("${field.name}" IN (${enumVals.map(v => `'${escapeSingleQuotes(v)}'`).join(',')}))`; + } else { + typeName = originalTypeName; + isBoolean = isBooleanType(originalTypeName); + } + } else { + typeName = 'TEXT'; + } + + let line = `"${field.name}" ${typeName}`; + + if (field.increment) { + if (typeName.toUpperCase().includes('INT')) { + line = `"${field.name}" INTEGER PRIMARY KEY AUTOINCREMENT`; + } + } else { + if (field.unique) line += ' UNIQUE'; + if (field.pk) line += ' PRIMARY KEY'; + if (field.not_null) line += ' NOT NULL'; + } + + if (field.dbdefault) { + if (field.dbdefault.type === 'expression') { + let expr = String(field.dbdefault.value || '').trim(); + line += ` DEFAULT (${expr})`; + } else if (field.dbdefault.type === 'string') { + line += ` DEFAULT '${escapeSingleQuotes(field.dbdefault.value)}'`; + } else { + line += ` DEFAULT ${field.dbdefault.value}`; + } + } + + if (!field.increment && isBoolean) { + line += ` CHECK ("${field.name}" IN (0,1))`; + } + if (enumCheck) line += enumCheck; + + return line; + }); + + return lines; + } + + static getCompositePKs(tableId, model) { + const table = model.tables[tableId]; + const compositePkIds = table.indexIds ? table.indexIds.filter(indexId => model.indexes[indexId].pk) : []; + const lines = compositePkIds.map((keyId) => { + const key = model.indexes[keyId]; + const columnArr = []; + + key.columnIds.forEach((columnId) => { + const column = model.indexColumns[columnId]; + let columnStr = ''; + if (column.type === 'expression') { + columnStr = `(${column.value})`; + } else { + columnStr = `"${column.value}"`; + } + columnArr.push(columnStr); + }); + + return `PRIMARY KEY (${columnArr.join(',')})`; + }); + return lines; + } + + static exportTables(tableIds, model, enumMap, fksByTableId) { + const tableStrs = (tableIds || []).map((tableId) => { + const table = model.tables[tableId]; + + const fieldContents = SqliteExporter.getFieldLines(tableId, model, enumMap); + const compositePKs = SqliteExporter.getCompositePKs(tableId, model); + const fkClauses = fksByTableId.get(tableId) || []; + + const content = [...fieldContents, ...compositePKs, ...fkClauses]; + + const tableStr = + `CREATE TABLE "${table.name}" (\n` + + `${content.map(line => ` ${line}`).join(',\n')}\n` + + `);\n`; + + return tableStr; + }); + + return tableStrs; + } + + static exportIndexes(indexIds, model) { + const indexArr = (indexIds || []).filter((indexId) => !model.indexes[indexId].pk).map((indexId) => { + const index = model.indexes[indexId]; + const table = model.tables[index.tableId]; + + let line = 'CREATE'; + if (index.unique) line += ' UNIQUE'; + const indexName = index.name ? `"${index.name}"` : ''; + line += ' INDEX'; + if (indexName) line += ` ${indexName}`; + line += ` ON "${table.name}"`; + + const columnArr = []; + index.columnIds.forEach((columnId) => { + const column = model.indexColumns[columnId]; + let columnStr = ''; + if (column.type === 'expression') { + columnStr = `(${column.value})`; + } else { + columnStr = `"${column.value}"`; + } + columnArr.push(columnStr); + }); + + line += ` (${columnArr.join(',')})`; + line += ';\n'; + return line; + }); + + return indexArr; + } + + static export(model) { + const database = model.database['1']; + + const enumMap = SqliteExporter.buildEnumMap(model); + + const allRefIds = _.flatten(database.schemaIds.map(sid => model.schemas[sid].refIds || [])); + const { fksByTableId, junctionCreates, dependencyEdges } = SqliteExporter.collectForeignKeysByTable(allRefIds, model); + + const allTableIds = _.flatten(database.schemaIds.map(sid => model.schemas[sid].tableIds || [])); + const orderedTableIds = topoSortTables(allTableIds, dependencyEdges); + + const tableCreates = SqliteExporter.exportTables(orderedTableIds, model, enumMap, fksByTableId); + + const allIndexIds = _.flatten( + (database.schemaIds || []).map(sid => { + const tIds = model.schemas[sid].tableIds || []; + return _.flatten(tIds.map(tid => model.tables[tid].indexIds || [])); + }) + ); + const indexCreates = SqliteExporter.exportIndexes(allIndexIds, model); + + const pragmas = ['PRAGMA foreign_keys = ON;\n']; + + const res = _.concat( + pragmas, + tableCreates, + junctionCreates, + indexCreates, + ).join('\n'); + + return res; + } +} + +export default SqliteExporter; \ No newline at end of file