sqlite-dialect.vala 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943
  1. using Invercargill.DataStructures;
  2. using Invercargill.Expressions;
  3. using InvercargillSql.Migrations;
  4. using InvercargillSql.Orm;
  5. using InvercargillSql.Orm.Projections;
  6. namespace InvercargillSql.Dialects {
  7. /**
  8. * SQLite implementation of the SqlDialect interface.
  9. *
  10. * SqliteDialect provides SQLite-specific type translations,
  11. * SQL generation methods, and schema introspection for SQLite databases.
  12. */
  13. public class SqliteDialect : Object, SqlDialect {
  14. /**
  15. * Translates a ColumnType to SQLite's type system.
  16. *
  17. * SQLite uses dynamic typing but these are the recommended
  18. * storage classes for each column type.
  19. *
  20. * @param type The column type to translate
  21. * @return The SQLite type string
  22. */
  23. public string translate_type(ColumnType type) {
  24. switch (type) {
  25. case ColumnType.INT_32:
  26. case ColumnType.INT_64:
  27. case ColumnType.BOOLEAN:
  28. return "INTEGER";
  29. case ColumnType.TEXT:
  30. case ColumnType.UUID:
  31. return "TEXT";
  32. case ColumnType.DECIMAL:
  33. return "REAL";
  34. case ColumnType.DATETIME:
  35. // Unix epoch stored as integer
  36. return "INTEGER";
  37. case ColumnType.BINARY:
  38. return "BLOB";
  39. default:
  40. return "TEXT";
  41. }
  42. }
  43. /**
  44. * Translates an expression tree to SQL.
  45. *
  46. * Currently returns empty string - will be implemented
  47. * with ExpressionToSqlVisitor in a future phase.
  48. *
  49. * @param expr The expression to translate
  50. * @param mapper The entity mapper for column resolution
  51. * @return The SQL string representation
  52. */
  53. public string translate_expression(Expression expr, EntityMapper mapper) {
  54. // For now, return empty - will be implemented with ExpressionToSqlVisitor
  55. return "";
  56. }
  57. /**
  58. * Builds a SELECT statement for all rows in a table.
  59. *
  60. * @param table_name The name of the table
  61. * @return The SQL SELECT statement
  62. */
  63. public string build_select_all(string table_name) {
  64. return "SELECT * FROM %s".printf(table_name);
  65. }
  66. /**
  67. * Builds a SELECT statement for a single row by ID.
  68. *
  69. * @param table_name The name of the table
  70. * @param id_column The name of the ID column
  71. * @return The SQL SELECT statement with named parameter placeholder
  72. */
  73. public string build_select_by_id(string table_name, string id_column) {
  74. return "SELECT * FROM %s WHERE %s = :%s".printf(table_name, id_column, id_column);
  75. }
  76. /**
  77. * Builds an INSERT statement with named parameter placeholders.
  78. *
  79. * @param table_name The name of the table
  80. * @param columns The column names to insert into
  81. * @return The SQL INSERT statement with named parameter placeholders
  82. */
  83. public string build_insert_sql(string table_name, Vector<string> columns) {
  84. var cols = new string[0];
  85. var placeholders = new string[0];
  86. foreach (var col in columns) {
  87. cols += col;
  88. placeholders += ":" + col;
  89. }
  90. return "INSERT INTO %s (%s) VALUES (%s)".printf(
  91. table_name,
  92. string.joinv(", ", cols),
  93. string.joinv(", ", placeholders)
  94. );
  95. }
  96. /**
  97. * Builds an UPDATE statement with named parameter placeholders.
  98. *
  99. * The ID column is excluded from the SET clause and used
  100. * only in the WHERE clause.
  101. *
  102. * @param table_name The name of the table
  103. * @param columns The column names to update
  104. * @param id_column The name of the ID column for the WHERE clause
  105. * @return The SQL UPDATE statement with named parameter placeholders
  106. */
  107. public string build_update_sql(string table_name, Vector<string> columns, string id_column) {
  108. var set_clauses = new string[0];
  109. foreach (var col in columns) {
  110. if (col != id_column) {
  111. set_clauses += "%s = :%s".printf(col, col);
  112. }
  113. }
  114. return "UPDATE %s SET %s WHERE %s = :%s".printf(
  115. table_name,
  116. string.joinv(", ", set_clauses),
  117. id_column,
  118. id_column
  119. );
  120. }
  121. /**
  122. * Builds a DELETE statement with named parameter placeholder.
  123. *
  124. * @param table_name The name of the table
  125. * @param id_column The name of the ID column for the WHERE clause
  126. * @return The SQL DELETE statement with named parameter placeholder
  127. */
  128. public string build_delete_sql(string table_name, string id_column) {
  129. return "DELETE FROM %s WHERE %s = :%s".printf(table_name, id_column, id_column);
  130. }
  131. /**
  132. * Builds a CREATE TABLE statement.
  133. *
  134. * @param table_name The name of the table
  135. * @param columns The column definitions
  136. * @return The SQL CREATE TABLE statement
  137. */
  138. public string build_create_table(string table_name, Vector<ColumnDefinition> columns) {
  139. var column_defs = new string[0];
  140. foreach (var col in columns) {
  141. column_defs += build_column_def(col);
  142. }
  143. return "CREATE TABLE %s (%s)".printf(
  144. table_name,
  145. string.joinv(", ", column_defs)
  146. );
  147. }
  148. /**
  149. * Builds a CREATE INDEX statement.
  150. *
  151. * @param table_name The name of the table
  152. * @param index The index definition
  153. * @return The SQL CREATE INDEX statement
  154. */
  155. public string build_create_index(string table_name, IndexDefinition index) {
  156. var unique = index.is_unique ? "UNIQUE " : "";
  157. var cols = new string[0];
  158. foreach (var col in index.columns) {
  159. cols += col;
  160. }
  161. return "CREATE %sINDEX %s ON %s (%s)".printf(
  162. unique,
  163. index.name,
  164. table_name,
  165. string.joinv(", ", cols)
  166. );
  167. }
  168. /**
  169. * Builds a single column definition for CREATE TABLE.
  170. *
  171. * @param col The column definition
  172. * @return The SQL column definition string
  173. */
  174. private string build_column_def(ColumnDefinition col) {
  175. var parts = new string[0];
  176. parts += col.name;
  177. parts += translate_type(col.column_type);
  178. if (col.is_primary_key) {
  179. parts += "PRIMARY KEY";
  180. }
  181. if (col.auto_increment && col.is_primary_key) {
  182. parts += "AUTOINCREMENT";
  183. }
  184. if (col.is_required && !col.is_primary_key) {
  185. parts += "NOT NULL";
  186. }
  187. if (col.is_unique && !col.is_primary_key) {
  188. parts += "UNIQUE";
  189. }
  190. if (col.default_now) {
  191. parts += "DEFAULT (strftime('%%s', 'now'))";
  192. }
  193. return string.joinv(" ", parts);
  194. }
  195. // Migration DDL method implementations
  196. /**
  197. * Generates SQL for a CREATE TABLE operation with constraints.
  198. *
  199. * @param op The create table operation
  200. * @return The SQL CREATE TABLE statement
  201. */
  202. public string create_table_sql(CreateTableOperation op) {
  203. var sql = new StringBuilder();
  204. sql.append("CREATE TABLE ");
  205. sql.append(op.table_name);
  206. sql.append(" (\n");
  207. var parts = new Vector<string>();
  208. foreach (var col in op.columns) {
  209. parts.add(build_column_sql(col));
  210. }
  211. foreach (var constraint in op.constraints) {
  212. parts.add(build_constraint_sql(constraint));
  213. }
  214. bool first = true;
  215. foreach (var part in parts) {
  216. if (!first) {
  217. sql.append(",\n");
  218. }
  219. first = false;
  220. sql.append(" ");
  221. sql.append(part);
  222. }
  223. sql.append("\n)");
  224. return sql.str;
  225. }
  226. /**
  227. * Builds a column definition SQL string for migration operations.
  228. *
  229. * @param col The column definition
  230. * @return The SQL column definition string
  231. */
  232. private string build_column_sql(ColumnDefinition col) {
  233. var parts = new Vector<string>();
  234. parts.add(col.name);
  235. parts.add(translate_type(col.column_type));
  236. if (col.is_primary_key) parts.add("PRIMARY KEY");
  237. if (col.auto_increment) parts.add("AUTOINCREMENT");
  238. if (col.is_required) parts.add("NOT NULL");
  239. if (col.is_unique) parts.add("UNIQUE");
  240. if (col.default_value != null) {
  241. parts.add("DEFAULT " + element_to_sql(col.default_value));
  242. }
  243. if (col.default_now) {
  244. parts.add("DEFAULT (strftime('%s', 'now'))");
  245. }
  246. return string.joinv(" ", parts.to_array());
  247. }
  248. /**
  249. * Converts an Element to its SQL representation for default values.
  250. *
  251. * @param element The element to convert
  252. * @return The SQL string representation
  253. */
  254. private string element_to_sql(Invercargill.Element element) {
  255. if (element.is_null()) {
  256. return "NULL";
  257. }
  258. // Try string first (needs quoting)
  259. if (element.assignable_to_type(typeof(string))) {
  260. string? s;
  261. if (element.try_get_as<string>(out s) && s != null) {
  262. return "'" + s.replace("'", "''") + "'";
  263. }
  264. }
  265. // Try bool (convert to 0/1)
  266. if (element.assignable_to_type(typeof(bool))) {
  267. bool? b;
  268. if (element.try_get_as<bool>(out b) && b != null) {
  269. return b ? "1" : "0";
  270. }
  271. }
  272. // Try int64
  273. if (element.assignable_to_type(typeof(int64))) {
  274. int64? i;
  275. if (element.try_get_as<int64?>(out i) && i != null) {
  276. return i.to_string();
  277. }
  278. }
  279. // Try int
  280. if (element.assignable_to_type(typeof(int))) {
  281. int? i;
  282. if (element.try_get_as<int?>(out i) && i != null) {
  283. return i.to_string();
  284. }
  285. }
  286. // Try long
  287. if (element.assignable_to_type(typeof(long))) {
  288. long? l;
  289. if (element.try_get_as<long?>(out l) && l != null) {
  290. return l.to_string();
  291. }
  292. }
  293. // Try double
  294. if (element.assignable_to_type(typeof(double))) {
  295. double? d;
  296. if (element.try_get_as<double?>(out d) && d != null) {
  297. return d.to_string();
  298. }
  299. }
  300. // Fallback to NULL
  301. return "NULL";
  302. }
  303. /**
  304. * Builds a table constraint SQL string.
  305. *
  306. * @param constraint The table constraint
  307. * @return The SQL constraint string
  308. */
  309. private string build_constraint_sql(TableConstraint constraint) {
  310. var sql = new StringBuilder();
  311. if (constraint.name != null) {
  312. sql.append("CONSTRAINT ");
  313. sql.append(constraint.name);
  314. sql.append(" ");
  315. }
  316. sql.append(constraint.constraint_type);
  317. sql.append(" (");
  318. sql.append(string.joinv(", ", constraint.columns.to_array()));
  319. sql.append(")");
  320. if (constraint.constraint_type == "FOREIGN KEY" && constraint.reference_table != null) {
  321. sql.append(" REFERENCES ");
  322. sql.append(constraint.reference_table);
  323. sql.append(" (");
  324. sql.append(string.joinv(", ", constraint.reference_columns.to_array()));
  325. sql.append(")");
  326. // Add ON DELETE action if not NO_ACTION (default)
  327. if (constraint.on_delete_action != ReferentialAction.NO_ACTION) {
  328. sql.append(" ON DELETE ");
  329. sql.append(constraint.on_delete_action.to_sql());
  330. }
  331. // Add ON UPDATE action if not NO_ACTION (default)
  332. if (constraint.on_update_action != ReferentialAction.NO_ACTION) {
  333. sql.append(" ON UPDATE ");
  334. sql.append(constraint.on_update_action.to_sql());
  335. }
  336. }
  337. return sql.str;
  338. }
  339. /**
  340. * Generates SQL for a DROP TABLE operation.
  341. *
  342. * @param op The drop table operation
  343. * @return The SQL DROP TABLE statement
  344. */
  345. public string drop_table_sql(DropTableOperation op) {
  346. return "DROP TABLE IF EXISTS " + op.table_name;
  347. }
  348. /**
  349. * Generates SQL for an ADD COLUMN operation.
  350. *
  351. * When the operation has a foreign_key_constraint, the REFERENCES clause
  352. * is included inline in the column definition.
  353. *
  354. * @param op The add column operation
  355. * @return The SQL ALTER TABLE ADD COLUMN statement
  356. */
  357. public string add_column_sql(AddColumnOperation op) {
  358. var sb = new StringBuilder();
  359. sb.append("ALTER TABLE ");
  360. sb.append(op.table_name);
  361. sb.append(" ADD COLUMN ");
  362. sb.append(build_column_sql(op.column));
  363. // Add inline REFERENCES if FK constraint is set
  364. if (op.foreign_key_constraint != null) {
  365. var fk = op.foreign_key_constraint;
  366. sb.append(" REFERENCES ");
  367. sb.append(fk.reference_table);
  368. sb.append(" (");
  369. sb.append(string.joinv(", ", fk.reference_columns.to_array()));
  370. sb.append(")");
  371. if (fk.on_delete_action != ReferentialAction.NO_ACTION) {
  372. sb.append(" ON DELETE ");
  373. sb.append(fk.on_delete_action.to_sql());
  374. }
  375. if (fk.on_update_action != ReferentialAction.NO_ACTION) {
  376. sb.append(" ON UPDATE ");
  377. sb.append(fk.on_update_action.to_sql());
  378. }
  379. }
  380. return sb.str;
  381. }
  382. /**
  383. * Generates SQL for a DROP COLUMN operation.
  384. *
  385. * SQLite 3.35.0+ supports DROP COLUMN natively.
  386. *
  387. * @param op The drop column operation
  388. * @return The SQL ALTER TABLE DROP COLUMN statement
  389. */
  390. public string drop_column_sql(DropColumnOperation op) {
  391. return "ALTER TABLE " + op.table_name + " DROP COLUMN " + op.column_name;
  392. }
  393. /**
  394. * Generates SQL for a RENAME COLUMN operation.
  395. *
  396. * @param op The rename column operation
  397. * @return The SQL ALTER TABLE RENAME COLUMN statement
  398. */
  399. public string rename_column_sql(RenameColumnOperation op) {
  400. return "ALTER TABLE " + op.table_name + " RENAME COLUMN " + op.old_name + " TO " + op.new_name;
  401. }
  402. /**
  403. * Generates SQL for a CREATE INDEX operation.
  404. *
  405. * @param op The create index operation
  406. * @return The SQL CREATE INDEX statement
  407. */
  408. public string create_index_sql(CreateIndexOperation op) {
  409. var sql = new StringBuilder();
  410. sql.append(op.is_unique ? "CREATE UNIQUE INDEX " : "CREATE INDEX ");
  411. sql.append(op.index_name);
  412. sql.append(" ON ");
  413. sql.append(op.table_name);
  414. sql.append(" (");
  415. sql.append(string.joinv(", ", op.columns.to_array()));
  416. sql.append(")");
  417. return sql.str;
  418. }
  419. /**
  420. * Generates SQL for a DROP INDEX operation.
  421. *
  422. * @param op The drop index operation
  423. * @return The SQL DROP INDEX statement
  424. */
  425. public string drop_index_sql(DropIndexOperation op) {
  426. return "DROP INDEX IF EXISTS " + op.index_name;
  427. }
  428. /**
  429. * Generates SQL for a DROP FOREIGN KEY operation.
  430. *
  431. * SQLite does not support ALTER TABLE DROP CONSTRAINT directly.
  432. * Instead, this method generates a special marker that is intercepted
  433. * by the SQLite internals (SqliteConnection.execute()) which then
  434. * performs transparent table recreation.
  435. *
  436. * Marker format: INVERCARGILL_SQL_SQLITE_DROP_CONSTRAINT(table='name',constraint='name')
  437. * or: INVERCARGILL_SQL_SQLITE_DROP_CONSTRAINT(table='name',column='col')
  438. *
  439. * @param op The drop foreign key operation
  440. * @return A special marker that triggers transparent table recreation
  441. */
  442. public string drop_foreign_key_sql(DropForeignKeyOperation op) {
  443. var sql = new StringBuilder();
  444. sql.append("INVERCARGILL_SQL_SQLITE_DROP_CONSTRAINT(");
  445. sql.append("table='");
  446. sql.append(escape_marker_value(op.table_name));
  447. sql.append("'");
  448. if (op.constraint_name != null) {
  449. sql.append(",constraint='");
  450. sql.append(escape_marker_value(op.constraint_name));
  451. sql.append("'");
  452. } else if (op.column_name != null) {
  453. sql.append(",column='");
  454. sql.append(escape_marker_value(op.column_name));
  455. sql.append("'");
  456. }
  457. sql.append(")");
  458. return sql.str;
  459. }
  460. /**
  461. * Escapes a value for use in a DROP CONSTRAINT marker.
  462. * Single quotes are escaped by doubling them.
  463. */
  464. private string escape_marker_value(string value) {
  465. return value.replace("'", "''");
  466. }
  467. /**
  468. * Generates SQL to drop a foreign key constraint using table recreation.
  469. *
  470. * This method requires schema introspection data (columns and foreign keys)
  471. * which must be obtained separately using PRAGMA commands on an active connection.
  472. *
  473. * The table recreation strategy:
  474. * 1. Create a new table (_table_new) with all columns but without the dropped FK
  475. * 2. Copy all data from the old table
  476. * 3. Drop the old table
  477. * 4. Rename the new table to the original name
  478. * 5. Recreate any indexes
  479. *
  480. * @param table_name The table containing the FK constraint
  481. * @param columns The column definitions for the recreated table
  482. * @param remaining_constraints All constraints EXCEPT the one being dropped
  483. * @param index_definitions Indexes to recreate after table recreation
  484. * @return SQL statements for table recreation (semicolon-separated)
  485. */
  486. public string generate_drop_fk_table_recreation_sql(
  487. string table_name,
  488. Vector<ColumnDefinition> columns,
  489. Vector<TableConstraint> remaining_constraints,
  490. Vector<CreateIndexOperation> index_definitions
  491. ) {
  492. var sql = new StringBuilder();
  493. string new_table_name = @"_$(table_name)_new";
  494. // Step 1: Create the new table without the dropped FK
  495. sql.append("CREATE TABLE ");
  496. sql.append(quote_identifier(new_table_name));
  497. sql.append(" (\n");
  498. var parts = new Vector<string>();
  499. foreach (var col in columns) {
  500. parts.add(build_column_sql(col));
  501. }
  502. foreach (var constraint in remaining_constraints) {
  503. parts.add(build_constraint_sql(constraint));
  504. }
  505. bool first = true;
  506. foreach (var part in parts) {
  507. if (!first) {
  508. sql.append(",\n");
  509. }
  510. first = false;
  511. sql.append(" ");
  512. sql.append(part);
  513. }
  514. sql.append("\n);\n\n");
  515. // Step 2: Copy data from old table to new table
  516. var column_names = new Vector<string>();
  517. foreach (var col in columns) {
  518. column_names.add(col.name);
  519. }
  520. sql.append("INSERT INTO ");
  521. sql.append(quote_identifier(new_table_name));
  522. sql.append(" (");
  523. sql.append(string.joinv(", ", column_names.to_array()));
  524. sql.append(")\nSELECT ");
  525. sql.append(string.joinv(", ", column_names.to_array()));
  526. sql.append(" FROM ");
  527. sql.append(quote_identifier(table_name));
  528. sql.append(";\n\n");
  529. // Step 3: Drop the old table
  530. sql.append("DROP TABLE ");
  531. sql.append(quote_identifier(table_name));
  532. sql.append(";\n\n");
  533. // Step 4: Rename the new table
  534. sql.append("ALTER TABLE ");
  535. sql.append(quote_identifier(new_table_name));
  536. sql.append(" RENAME TO ");
  537. sql.append(quote_identifier(table_name));
  538. sql.append(";\n");
  539. // Step 5: Recreate indexes
  540. foreach (var index_op in index_definitions) {
  541. sql.append("\n");
  542. sql.append(create_index_sql(index_op));
  543. sql.append(";");
  544. }
  545. return sql.str;
  546. }
  547. /**
  548. * Quotes an identifier for use in SQL.
  549. * Uses double quotes for SQLite identifier quoting.
  550. */
  551. private string quote_identifier(string identifier) {
  552. return "\"" + identifier.replace("\"", "\"\"") + "\"";
  553. }
  554. // Schema introspection implementation
  555. /**
  556. * Introspects table schema from a SQLite database.
  557. *
  558. * This method uses PRAGMA table_info() to discover column metadata
  559. * and sqlite_master to identify primary keys.
  560. *
  561. * @param connection The database connection
  562. * @param table_name The table to introspect
  563. * @return A TableSchema containing column metadata
  564. * @throws SqlError if introspection fails
  565. */
  566. public TableSchema introspect_schema(Connection connection, string table_name) throws SqlError {
  567. var schema = new TableSchema();
  568. schema.table_name = table_name;
  569. // Query column information using PRAGMA table_info
  570. var pragma_sql = "PRAGMA table_info(%s)".printf(table_name);
  571. var command = connection.create_command(pragma_sql);
  572. var results = command.execute_query();
  573. foreach (var row in results) {
  574. var column = new ColumnSchema();
  575. // PRAGMA table_info columns: cid, name, type, notnull, dflt_value, pk
  576. var name_elem = row.get("name");
  577. var type_elem = row.get("type");
  578. var notnull_elem = row.get("notnull");
  579. var pk_elem = row.get("pk");
  580. if (name_elem != null) {
  581. string? name_val = null;
  582. name_elem.try_get_as<string>(out name_val);
  583. column.name = name_val ?? "";
  584. }
  585. if (type_elem != null) {
  586. string? type_str = null;
  587. type_elem.try_get_as<string>(out type_str);
  588. column.column_type = parse_sqlite_type(type_str ?? "TEXT");
  589. }
  590. if (notnull_elem != null) {
  591. int64? notnull = null;
  592. notnull_elem.try_get_as<int64?>(out notnull);
  593. column.is_required = (notnull == 1);
  594. }
  595. if (pk_elem != null) {
  596. int64? pk = null;
  597. pk_elem.try_get_as<int64?>(out pk);
  598. column.is_primary_key = (pk != null && pk > 0);
  599. if (column.is_primary_key) {
  600. schema.primary_key_column = column.name;
  601. // Check for auto-increment by querying sqlite_sequence
  602. // In SQLite, INTEGER PRIMARY KEY is always auto-increment potential
  603. // but only actually auto-increments if the column is declared as such
  604. // For simplicity, we'll check if it's an INTEGER PRIMARY KEY
  605. if (column.column_type == ColumnType.INT_64 || column.column_type == ColumnType.INT_32) {
  606. column.auto_increment = true;
  607. }
  608. }
  609. }
  610. schema.columns.add(column);
  611. }
  612. return schema;
  613. }
  614. /**
  615. * Parses a SQLite type string to a ColumnType.
  616. *
  617. * SQLite uses type affinity, so we match based on keywords in the type name.
  618. *
  619. * @param type_str The SQLite type string
  620. * @return The corresponding ColumnType
  621. */
  622. private ColumnType parse_sqlite_type(string type_str) {
  623. var upper = type_str.up();
  624. if ("INT" in upper) {
  625. return ColumnType.INT_64;
  626. }
  627. if ("CHAR" in upper || "CLOB" in upper || "TEXT" in upper) {
  628. return ColumnType.TEXT;
  629. }
  630. if ("BLOB" in upper) {
  631. return ColumnType.BINARY;
  632. }
  633. if ("REAL" in upper || "FLOA" in upper || "DOUB" in upper) {
  634. return ColumnType.DECIMAL;
  635. }
  636. if ("DATE" in upper || "TIME" in upper) {
  637. return ColumnType.DATETIME;
  638. }
  639. if ("BOOL" in upper) {
  640. return ColumnType.BOOLEAN;
  641. }
  642. if ("UUID" in upper || "GUID" in upper) {
  643. return ColumnType.UUID;
  644. }
  645. // Default to TEXT
  646. return ColumnType.TEXT;
  647. }
  648. // Projection query method implementations
  649. /**
  650. * Builds a SELECT statement with JOINs for a projection query.
  651. *
  652. * This method constructs a complete SELECT query from a ProjectionDefinition,
  653. * including all JOINs, selections, WHERE/HAVING clauses, GROUP BY, ORDER BY,
  654. * LIMIT, and OFFSET.
  655. *
  656. * @param definition The projection definition containing source, joins, and selections
  657. * @param translator The variable translator for alias resolution
  658. * @param where_clause Optional pre-built WHERE clause SQL
  659. * @param having_clause Optional pre-built HAVING clause SQL
  660. * @param order_by ORDER BY clauses
  661. * @param limit Optional LIMIT value
  662. * @param offset Optional OFFSET value
  663. * @return The complete SQL SELECT statement
  664. */
  665. public string build_projection_select(
  666. ProjectionDefinition definition,
  667. VariableTranslator translator,
  668. string? where_clause,
  669. string? having_clause,
  670. Vector<OrderByClause> order_by,
  671. int64? limit,
  672. int64? offset
  673. ) {
  674. var sql = new StringBuilder();
  675. // Build SELECT clause with all selections
  676. sql.append("SELECT ");
  677. build_select_clause(sql, definition, translator);
  678. // Build FROM clause with primary source
  679. sql.append("\nFROM ");
  680. build_from_clause(sql, definition, translator);
  681. // Build JOIN clauses for all joins
  682. foreach (var join in definition.joins) {
  683. sql.append("\n");
  684. build_join_clause(sql, join, translator);
  685. }
  686. // Build WHERE clause (if provided)
  687. if (where_clause != null && where_clause.length > 0) {
  688. sql.append("\nWHERE ");
  689. sql.append(where_clause);
  690. }
  691. // Build GROUP BY clause (if any group_by expressions)
  692. if (definition.group_by_expressions.length > 0) {
  693. sql.append("\nGROUP BY ");
  694. bool first = true;
  695. foreach (var group_expr in definition.group_by_expressions) {
  696. if (!first) {
  697. sql.append(", ");
  698. }
  699. first = false;
  700. sql.append(translator.translate_expression(group_expr));
  701. }
  702. }
  703. // Build HAVING clause (if provided)
  704. if (having_clause != null && having_clause.length > 0) {
  705. sql.append("\nHAVING ");
  706. sql.append(having_clause);
  707. }
  708. // Build ORDER BY clause (if any)
  709. if (order_by.length > 0) {
  710. sql.append("\nORDER BY ");
  711. bool first = true;
  712. foreach (var order in order_by) {
  713. if (!first) {
  714. sql.append(", ");
  715. }
  716. first = false;
  717. sql.append(order.expression);
  718. if (order.descending) {
  719. sql.append(" DESC");
  720. }
  721. }
  722. }
  723. // Build LIMIT/OFFSET (if provided)
  724. if (limit != null) {
  725. sql.append("\nLIMIT ");
  726. sql.append(limit.to_string());
  727. }
  728. if (offset != null) {
  729. sql.append("\nOFFSET ");
  730. sql.append(offset.to_string());
  731. }
  732. return sql.str;
  733. }
  734. /**
  735. * Builds the SELECT clause with all selections.
  736. *
  737. * Each selection is formatted as: expression AS friendly_name
  738. *
  739. * @param sql The StringBuilder to append to
  740. * @param definition The projection definition
  741. * @param translator The variable translator for alias resolution
  742. */
  743. private void build_select_clause(
  744. StringBuilder sql,
  745. ProjectionDefinition definition,
  746. VariableTranslator translator
  747. ) {
  748. bool first = true;
  749. foreach (var selection in definition.selections) {
  750. if (!first) {
  751. sql.append(", ");
  752. }
  753. first = false;
  754. // For scalar selections, translate the expression
  755. // For nested/collection selections, we'll handle them differently
  756. var scalar_selection = selection as ScalarSelection<Object, Object>;
  757. if (scalar_selection != null) {
  758. // Translate the expression using the variable translator
  759. string translated_expr = translator.translate_expression(scalar_selection.expression);
  760. sql.append(translated_expr);
  761. sql.append(" AS ");
  762. sql.append(selection.friendly_name);
  763. } else {
  764. // For nested/collection projections, select a placeholder
  765. // The actual nested data will be fetched separately
  766. sql.append("NULL AS ");
  767. sql.append(selection.friendly_name);
  768. }
  769. }
  770. }
  771. /**
  772. * Builds the FROM clause with the primary source.
  773. *
  774. * Format: table_name alias
  775. *
  776. * @param sql The StringBuilder to append to
  777. * @param definition The projection definition
  778. * @param translator The variable translator for alias resolution
  779. */
  780. private void build_from_clause(
  781. StringBuilder sql,
  782. ProjectionDefinition definition,
  783. VariableTranslator translator
  784. ) {
  785. if (definition.source == null) {
  786. return;
  787. }
  788. sql.append(definition.source.table_name);
  789. sql.append(" ");
  790. sql.append(translator.translate_variable(definition.source.variable_name));
  791. }
  792. /**
  793. * Builds a JOIN clause for a join definition.
  794. *
  795. * Format: JOIN table_name alias ON condition
  796. *
  797. * @param sql The StringBuilder to append to
  798. * @param join The join definition
  799. * @param translator The variable translator for alias resolution
  800. */
  801. private void build_join_clause(
  802. StringBuilder sql,
  803. JoinDefinition join,
  804. VariableTranslator translator
  805. ) {
  806. sql.append("JOIN ");
  807. sql.append(join.table_name);
  808. sql.append(" ");
  809. sql.append(translator.translate_variable(join.variable_name));
  810. sql.append(" ON ");
  811. sql.append(translator.translate_expression(join.join_condition));
  812. }
  813. /**
  814. * Builds a subquery wrapper for mixed aggregate/non-aggregate OR conditions.
  815. *
  816. * When a WHERE clause contains OR conditions that mix aggregate and non-aggregate
  817. * expressions, SQL requires special handling. This method wraps the inner query
  818. * and applies the combined WHERE clause to the outer query.
  819. *
  820. * @param inner_query The inner SELECT query
  821. * @param combined_where The WHERE clause to apply to the outer query
  822. * @return The wrapped query SQL
  823. */
  824. public string wrap_subquery_for_mixed_or(
  825. string inner_query,
  826. string combined_where
  827. ) {
  828. return @"SELECT * FROM ($inner_query) subq WHERE $combined_where";
  829. }
  830. /**
  831. * Generates a table alias with type information for debugging.
  832. *
  833. * The alias format includes an index and the entity type name to make
  834. * generated SQL more readable and easier to debug.
  835. *
  836. * Format: val_N_TypeName (e.g., val_1_User, val_2_Order)
  837. *
  838. * @param index The 1-based index for this alias
  839. * @param type_name The entity type name
  840. * @return The generated alias string
  841. */
  842. public string generate_table_alias(int index, string type_name) {
  843. return "val_%d_%s".printf(index, type_name);
  844. }
  845. }
  846. }