postgres-dialect.vala 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  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. * PostgreSQL implementation of the SqlDialect interface: type translations,
  9. * SQL generation and schema introspection for PostgreSQL databases.
  10. *
  11. * Placeholders: the CRUD builders emit named ":column" placeholders, exactly
  12. * like the SQLite dialect. PostgresCommand rewrites ":name" placeholders to
  13. * positional "$n" references at execution time, numbering them in order of
  14. * first occurrence and binding values from the named parameter dictionary,
  15. * so OrmSession's by-name binding works unchanged. The shared
  16. * ExpressionToSqlVisitor likewise emits ":pN" placeholders which the adapter
  17. * translates the same way.
  18. */
  19. public class PostgresDialect : Object, SqlDialect {
  20. /** Translates a ColumnType to the PostgreSQL type system. */
  21. public string translate_type(ColumnType type) {
  22. switch (type) {
  23. case ColumnType.INT_32:
  24. return "INTEGER";
  25. case ColumnType.INT_64:
  26. return "BIGINT";
  27. case ColumnType.BOOLEAN:
  28. return "BOOLEAN";
  29. case ColumnType.TEXT:
  30. return "TEXT";
  31. case ColumnType.UUID:
  32. return "UUID";
  33. case ColumnType.DECIMAL:
  34. return "DOUBLE PRECISION";
  35. case ColumnType.DATETIME:
  36. // Timestamp with time zone (stored internally in UTC)
  37. return "TIMESTAMPTZ";
  38. case ColumnType.BINARY:
  39. return "BYTEA";
  40. default:
  41. return "TEXT";
  42. }
  43. }
  44. /**
  45. * Translates an expression tree to SQL.
  46. *
  47. * Mirrors SqliteDialect: filter expressions are translated at the call
  48. * sites (EntityQuery, ProjectionSqlBuilder) via ExpressionToSqlVisitor,
  49. * whose ":pN" placeholders PostgresCommand converts to "$n".
  50. */
  51. public string translate_expression(Expression expr, EntityMapper mapper) {
  52. return "";
  53. }
  54. /** Builds a SELECT statement for all rows in a table. */
  55. public string build_select_all(string table_name) {
  56. return "SELECT * FROM %s".printf(table_name);
  57. }
  58. /** Builds a SELECT for one row by ID with a named ":column" placeholder. */
  59. public string build_select_by_id(string table_name, string id_column) {
  60. return "SELECT * FROM %s WHERE %s = :%s".printf(table_name, id_column, id_column);
  61. }
  62. /**
  63. * Builds an INSERT with named ":column" placeholders.
  64. *
  65. * The adapter numbers them "$1..$n" in order of first occurrence, which
  66. * is the order of the columns vector, so binding is deterministic.
  67. */
  68. public string build_insert_sql(string table_name, Vector<string> columns) {
  69. var cols = new string[0];
  70. var placeholders = new string[0];
  71. foreach (var col in columns) {
  72. cols += col;
  73. placeholders += ":" + col;
  74. }
  75. return "INSERT INTO %s (%s) VALUES (%s)".printf(
  76. table_name,
  77. string.joinv(", ", cols),
  78. string.joinv(", ", placeholders)
  79. );
  80. }
  81. /**
  82. * Builds an UPDATE with named ":column" placeholders.
  83. *
  84. * The ID column is excluded from the SET clause and used only in the
  85. * WHERE clause.
  86. */
  87. public string build_update_sql(string table_name, Vector<string> columns, string id_column) {
  88. var set_clauses = new string[0];
  89. foreach (var col in columns) {
  90. if (col != id_column) {
  91. set_clauses += "%s = :%s".printf(col, col);
  92. }
  93. }
  94. return "UPDATE %s SET %s WHERE %s = :%s".printf(
  95. table_name,
  96. string.joinv(", ", set_clauses),
  97. id_column,
  98. id_column
  99. );
  100. }
  101. /** Builds a DELETE with a named ":column" placeholder for the ID. */
  102. public string build_delete_sql(string table_name, string id_column) {
  103. return "DELETE FROM %s WHERE %s = :%s".printf(table_name, id_column, id_column);
  104. }
  105. /**
  106. * Builds a CREATE TABLE statement.
  107. *
  108. * Uses IF NOT EXISTS so re-running schema creation is idempotent.
  109. */
  110. public string build_create_table(string table_name, Vector<ColumnDefinition> columns) {
  111. var column_defs = new string[0];
  112. foreach (var col in columns) {
  113. column_defs += build_column_def(col);
  114. }
  115. return "CREATE TABLE IF NOT EXISTS %s (%s)".printf(
  116. table_name,
  117. string.joinv(", ", column_defs)
  118. );
  119. }
  120. /** Builds a CREATE INDEX statement (IF NOT EXISTS for idempotency). */
  121. public string build_create_index(string table_name, IndexDefinition index) {
  122. var unique = index.is_unique ? "UNIQUE " : "";
  123. var cols = new string[0];
  124. foreach (var col in index.columns) {
  125. cols += col;
  126. }
  127. return "CREATE %sINDEX IF NOT EXISTS %s ON %s (%s)".printf(
  128. unique,
  129. index.name,
  130. table_name,
  131. string.joinv(", ", cols)
  132. );
  133. }
  134. /**
  135. * Builds a single column definition for CREATE TABLE.
  136. *
  137. * Auto-increment primary keys become GENERATED ALWAYS AS IDENTITY
  138. * columns (PostgreSQL 10+), sized to match the base type.
  139. */
  140. private string build_column_def(ColumnDefinition col) {
  141. var parts = new string[0];
  142. parts += col.name;
  143. parts += translate_type(col.column_type);
  144. if (col.is_primary_key) {
  145. parts += "PRIMARY KEY";
  146. }
  147. if (col.auto_increment && col.is_primary_key) {
  148. parts += "GENERATED ALWAYS AS IDENTITY";
  149. }
  150. if (col.is_required && !col.is_primary_key) {
  151. parts += "NOT NULL";
  152. }
  153. if (col.is_unique && !col.is_primary_key) {
  154. parts += "UNIQUE";
  155. }
  156. if (col.default_now) {
  157. parts += "DEFAULT CURRENT_TIMESTAMP";
  158. }
  159. return string.joinv(" ", parts);
  160. }
  161. /**
  162. * Generates SQL for a CREATE TABLE migration operation with constraints.
  163. *
  164. * Multi-column PRIMARY KEY / UNIQUE / FOREIGN KEY constraints are
  165. * emitted as table constraints after the column definitions.
  166. */
  167. public string create_table_sql(CreateTableOperation op) {
  168. var sql = new StringBuilder();
  169. sql.append("CREATE TABLE ");
  170. sql.append(op.table_name);
  171. sql.append(" (\n");
  172. var parts = new Vector<string>();
  173. foreach (var col in op.columns) {
  174. parts.add(build_column_sql(col));
  175. }
  176. foreach (var constraint in op.constraints) {
  177. parts.add(build_constraint_sql(constraint));
  178. }
  179. bool first = true;
  180. foreach (var part in parts) {
  181. if (!first) {
  182. sql.append(",\n");
  183. }
  184. first = false;
  185. sql.append(" ");
  186. sql.append(part);
  187. }
  188. sql.append("\n)");
  189. return sql.str;
  190. }
  191. /**
  192. * Builds a column definition SQL string for migration operations.
  193. *
  194. * An auto-increment integer primary key becomes a GENERATED ALWAYS AS
  195. * IDENTITY column (PostgreSQL 10+). IDENTITY is skipped when the
  196. * column also carries an explicit DEFAULT, which PostgreSQL forbids.
  197. */
  198. private string build_column_sql(ColumnDefinition col) {
  199. var parts = new Vector<string>();
  200. parts.add(col.name);
  201. parts.add(translate_type(col.column_type));
  202. if (col.is_primary_key) parts.add("PRIMARY KEY");
  203. if (col.auto_increment && col.default_value == null && !col.default_now) {
  204. parts.add("GENERATED ALWAYS AS IDENTITY");
  205. }
  206. if (col.is_required) parts.add("NOT NULL");
  207. if (col.is_unique) parts.add("UNIQUE");
  208. if (col.default_value != null) {
  209. parts.add("DEFAULT " + element_to_sql(col.default_value));
  210. }
  211. if (col.default_now) {
  212. parts.add("DEFAULT CURRENT_TIMESTAMP");
  213. }
  214. return string.joinv(" ", parts.to_array());
  215. }
  216. /**
  217. * Converts an Element default value to its SQL literal representation.
  218. *
  219. * Booleans render as TRUE/FALSE (not 1/0), timestamps as ISO 8601
  220. * text, binary data as hex-encoded BYTEA literals, and strings are
  221. * escaped by doubling single quotes. The E'' literal form is used
  222. * when the value contains a backslash.
  223. */
  224. private string element_to_sql(Invercargill.Element element) {
  225. if (element.is_null()) {
  226. return "NULL";
  227. }
  228. if (element.assignable_to_type(typeof(Invercargill.BinaryData))) {
  229. Invercargill.BinaryData? blob;
  230. if (element.try_get_as<Invercargill.BinaryData>(out blob) && blob != null) {
  231. uint8[] blob_data = blob.to_array();
  232. return "\\\\x" + bytes_to_hex(blob_data);
  233. }
  234. }
  235. if (element.assignable_to_type(typeof(bool))) {
  236. bool? b;
  237. if (element.try_get_as<bool>(out b) && b != null) {
  238. return b ? "TRUE" : "FALSE";
  239. }
  240. }
  241. if (element.assignable_to_type(typeof(DateTime))) {
  242. DateTime? dt;
  243. if (element.try_get_as<DateTime>(out dt) && dt != null) {
  244. return "'" + dt.format_iso8601() + "'";
  245. }
  246. }
  247. string? result = try_element_as_string(element);
  248. if (result != null) {
  249. return quote_string_literal(result);
  250. }
  251. return "NULL";
  252. }
  253. /**
  254. * Renders a SQL string literal, using E'' escaping when the value
  255. * contains a backslash (PostgreSQL treats backslashes specially in
  256. * ordinary literals when standard_conforming_strings is off).
  257. */
  258. private string quote_string_literal(string s) {
  259. string escaped = s.replace("'", "''");
  260. if (s.index_of("\\") >= 0) {
  261. escaped = escaped.replace("\\", "\\\\");
  262. return "E'" + escaped + "'";
  263. }
  264. return "'" + escaped + "'";
  265. }
  266. /** Lowercase hex encoding of a byte array. */
  267. private string bytes_to_hex(uint8[] data) {
  268. var sb = new StringBuilder();
  269. foreach (uint8 b in data) {
  270. sb.append_printf("%02x", b);
  271. }
  272. return sb.str;
  273. }
  274. /** Extracts a numeric or string representation of an Element, or null. */
  275. private string? try_element_as_string(Invercargill.Element element) {
  276. if (element.assignable_to_type(typeof(string))) {
  277. string? s;
  278. if (element.try_get_as<string>(out s) && s != null) {
  279. return s;
  280. }
  281. }
  282. if (element.assignable_to_type(typeof(int64))) {
  283. int64? i;
  284. if (element.try_get_as<int64?>(out i) && i != null) {
  285. return i.to_string();
  286. }
  287. }
  288. if (element.assignable_to_type(typeof(int))) {
  289. int? i;
  290. if (element.try_get_as<int?>(out i) && i != null) {
  291. return i.to_string();
  292. }
  293. }
  294. if (element.assignable_to_type(typeof(long))) {
  295. long? l;
  296. if (element.try_get_as<long?>(out l) && l != null) {
  297. return l.to_string();
  298. }
  299. }
  300. if (element.assignable_to_type(typeof(double))) {
  301. double? d;
  302. if (element.try_get_as<double?>(out d) && d != null) {
  303. return d.to_string();
  304. }
  305. }
  306. return null;
  307. }
  308. /**
  309. * Builds a table constraint SQL string.
  310. *
  311. * Constraint types are the SQL-standard keywords ("PRIMARY KEY",
  312. * "UNIQUE", "FOREIGN KEY"), matching the SQLite implementation.
  313. */
  314. private string build_constraint_sql(TableConstraint constraint) {
  315. var sql = new StringBuilder();
  316. if (constraint.name != null) {
  317. sql.append("CONSTRAINT ");
  318. sql.append(constraint.name);
  319. sql.append(" ");
  320. }
  321. sql.append(constraint.constraint_type);
  322. sql.append(" (");
  323. sql.append(string.joinv(", ", constraint.columns.to_array()));
  324. sql.append(")");
  325. if (constraint.constraint_type == "FOREIGN KEY" && constraint.reference_table != null) {
  326. sql.append(" REFERENCES ");
  327. sql.append(constraint.reference_table);
  328. sql.append(" (");
  329. sql.append(string.joinv(", ", constraint.reference_columns.to_array()));
  330. sql.append(")");
  331. if (constraint.on_delete_action != ReferentialAction.NO_ACTION) {
  332. sql.append(" ON DELETE ");
  333. sql.append(constraint.on_delete_action.to_sql());
  334. }
  335. if (constraint.on_update_action != ReferentialAction.NO_ACTION) {
  336. sql.append(" ON UPDATE ");
  337. sql.append(constraint.on_update_action.to_sql());
  338. }
  339. }
  340. return sql.str;
  341. }
  342. /** Generates SQL for a DROP TABLE operation (IF EXISTS for idempotency). */
  343. public string drop_table_sql(DropTableOperation op) {
  344. return "DROP TABLE IF EXISTS " + op.table_name;
  345. }
  346. /**
  347. * Generates SQL for an ADD COLUMN operation.
  348. *
  349. * When the operation carries a foreign_key_constraint, the REFERENCES
  350. * clause is inlined in the column definition (PostgreSQL, unlike
  351. * SQLite, also accepts a separate ADD CONSTRAINT clause, but the
  352. * inline form keeps a single statement).
  353. */
  354. public string add_column_sql(AddColumnOperation op) {
  355. var sb = new StringBuilder();
  356. sb.append("ALTER TABLE ");
  357. sb.append(op.table_name);
  358. sb.append(" ADD COLUMN ");
  359. sb.append(build_column_sql(op.column));
  360. if (op.foreign_key_constraint != null) {
  361. var fk = op.foreign_key_constraint;
  362. sb.append(" REFERENCES ");
  363. sb.append(fk.reference_table);
  364. sb.append(" (");
  365. sb.append(string.joinv(", ", fk.reference_columns.to_array()));
  366. sb.append(")");
  367. if (fk.on_delete_action != ReferentialAction.NO_ACTION) {
  368. sb.append(" ON DELETE ");
  369. sb.append(fk.on_delete_action.to_sql());
  370. }
  371. if (fk.on_update_action != ReferentialAction.NO_ACTION) {
  372. sb.append(" ON UPDATE ");
  373. sb.append(fk.on_update_action.to_sql());
  374. }
  375. }
  376. return sb.str;
  377. }
  378. /** Generates SQL for a DROP COLUMN operation. */
  379. public string drop_column_sql(DropColumnOperation op) {
  380. return "ALTER TABLE " + op.table_name + " DROP COLUMN " + op.column_name;
  381. }
  382. /** Generates SQL for a RENAME COLUMN operation. */
  383. public string rename_column_sql(RenameColumnOperation op) {
  384. return "ALTER TABLE " + op.table_name + " RENAME COLUMN " + op.old_name + " TO " + op.new_name;
  385. }
  386. /** Generates SQL for a CREATE INDEX migration operation. */
  387. public string create_index_sql(CreateIndexOperation op) {
  388. var sql = new StringBuilder();
  389. sql.append(op.is_unique ? "CREATE UNIQUE INDEX " : "CREATE INDEX ");
  390. sql.append(op.index_name);
  391. sql.append(" ON ");
  392. sql.append(op.table_name);
  393. sql.append(" (");
  394. sql.append(string.joinv(", ", op.columns.to_array()));
  395. sql.append(")");
  396. return sql.str;
  397. }
  398. /** Generates SQL for a DROP INDEX migration operation (IF EXISTS). */
  399. public string drop_index_sql(DropIndexOperation op) {
  400. return "DROP INDEX IF EXISTS " + op.index_name;
  401. }
  402. /**
  403. * Generates SQL for a DROP FOREIGN KEY operation.
  404. *
  405. * PostgreSQL supports dropping constraints natively, so unlike the
  406. * SQLite dialect there is no table-recreation dance: the operation
  407. * maps directly to ALTER TABLE ... DROP CONSTRAINT. When the caller
  408. * identifies the constraint by column instead of by name, the
  409. * auto-generated name is derived the way PostgreSQL names FK
  410. * constraints (table_column_fkey).
  411. */
  412. public string drop_foreign_key_sql(DropForeignKeyOperation op) {
  413. var sb = new StringBuilder();
  414. sb.append("ALTER TABLE ");
  415. sb.append(op.table_name);
  416. sb.append(" DROP CONSTRAINT ");
  417. if (op.constraint_name != null) {
  418. sb.append(op.constraint_name);
  419. } else if (op.column_name != null) {
  420. // PostgreSQL names an unnamed FK constraint table_column_fkey
  421. sb.append(op.table_name);
  422. sb.append("_");
  423. sb.append(op.column_name);
  424. sb.append("_fkey");
  425. } else {
  426. // No usable identifier: keep the statement invalid on purpose
  427. // so the failure is visible rather than silently dropping
  428. // some other constraint.
  429. sb.append("<unnamed>");
  430. }
  431. return sb.str;
  432. }
  433. /** Quotes an identifier using PostgreSQL double-quote rules. */
  434. private string quote_identifier(string identifier) {
  435. return "\"" + identifier.replace("\"", "\"\"") + "\"";
  436. }
  437. // === Schema introspection ===
  438. /**
  439. * Introspects table schema from a PostgreSQL database.
  440. *
  441. * Column metadata comes from information_schema.columns; primary key
  442. * membership from information_schema.key_column_usage joined with
  443. * table_constraints. Identity columns (GENERATED ... AS IDENTITY) and
  444. * serial-style defaults are reported as auto-increment. Note that on
  445. * PostgreSQL 10+ identity columns report is_identity = 'YES' with an
  446. * empty column_default, so both signals are checked.
  447. *
  448. * @param connection The database connection
  449. * @param table_name The table to introspect
  450. * @return A TableSchema containing column metadata
  451. * @throws SqlError if introspection fails
  452. */
  453. public TableSchema introspect_schema(Connection connection, string table_name) throws SqlError {
  454. var schema = new TableSchema();
  455. schema.table_name = table_name;
  456. var columns_sql = new StringBuilder();
  457. columns_sql.append("SELECT column_name, data_type, is_nullable, column_default, is_identity ");
  458. columns_sql.append("FROM information_schema.columns ");
  459. columns_sql.append("WHERE table_name = :table_name ");
  460. columns_sql.append("ORDER BY ordinal_position");
  461. var command = connection.create_command(columns_sql.str);
  462. command.with_parameter<string>("table_name", table_name);
  463. var results = command.execute_query();
  464. foreach (var row in results) {
  465. var column = new ColumnSchema();
  466. var name_elem = row.get("column_name");
  467. if (name_elem != null) {
  468. string? name_val = null;
  469. name_elem.try_get_as<string>(out name_val);
  470. column.name = name_val ?? "";
  471. }
  472. var type_elem = row.get("data_type");
  473. if (type_elem != null) {
  474. string? type_str = null;
  475. type_elem.try_get_as<string>(out type_str);
  476. column.column_type = parse_postgres_type(type_str ?? "text");
  477. }
  478. var nullable_elem = row.get("is_nullable");
  479. if (nullable_elem != null) {
  480. string? nullable = null;
  481. nullable_elem.try_get_as<string>(out nullable);
  482. column.is_required = (nullable == "NO");
  483. }
  484. var default_elem = row.get("column_default");
  485. if (default_elem != null && !default_elem.is_null()) {
  486. string? default_str = null;
  487. default_elem.try_get_as<string>(out default_str);
  488. column.auto_increment = is_identity_default(default_str);
  489. }
  490. var identity_elem = row.get("is_identity");
  491. if (identity_elem != null && !identity_elem.is_null()) {
  492. string? identity_str = null;
  493. identity_elem.try_get_as<string>(out identity_str);
  494. if (identity_str == "YES") {
  495. column.auto_increment = true;
  496. }
  497. }
  498. schema.columns.add(column);
  499. }
  500. // Determine the primary key column(s)
  501. var pk_sql = new StringBuilder();
  502. pk_sql.append("SELECT kcu.column_name ");
  503. pk_sql.append("FROM information_schema.table_constraints tc ");
  504. pk_sql.append("JOIN information_schema.key_column_usage kcu ");
  505. pk_sql.append(" ON tc.constraint_name = kcu.constraint_name ");
  506. pk_sql.append(" AND tc.table_name = kcu.table_name ");
  507. pk_sql.append("WHERE tc.constraint_type = 'PRIMARY KEY' ");
  508. pk_sql.append(" AND tc.table_name = :table_name ");
  509. pk_sql.append("ORDER BY kcu.ordinal_position");
  510. var pk_command = connection.create_command(pk_sql.str);
  511. pk_command.with_parameter<string>("table_name", table_name);
  512. var pk_results = pk_command.execute_query();
  513. foreach (var pk_row in pk_results) {
  514. var pk_elem = pk_row.get("column_name");
  515. if (pk_elem == null) {
  516. continue;
  517. }
  518. string? pk_name = null;
  519. pk_elem.try_get_as<string>(out pk_name);
  520. if (pk_name == null) {
  521. continue;
  522. }
  523. var col = schema.get_column(pk_name);
  524. if (col != null) {
  525. col.is_primary_key = true;
  526. if (schema.primary_key_column == null) {
  527. schema.primary_key_column = pk_name;
  528. }
  529. // Integer PKs created by this dialect are identity columns
  530. if (col.column_type == ColumnType.INT_64
  531. || col.column_type == ColumnType.INT_32) {
  532. col.auto_increment = true;
  533. }
  534. }
  535. }
  536. return schema;
  537. }
  538. /**
  539. * Reports whether a column_default string denotes an auto-generated
  540. * identity value (IDENTITY columns or old-style serial sequences).
  541. */
  542. private bool is_identity_default(string? default_str) {
  543. if (default_str == null) {
  544. return false;
  545. }
  546. var upper = default_str.up();
  547. if ("GENERATED" in upper && "IDENTITY" in upper) {
  548. return true;
  549. }
  550. // serial pseudo-types surface as nextval('..._seq'::regclass)
  551. if ("NEXTVAL" in upper) {
  552. return true;
  553. }
  554. return false;
  555. }
  556. /**
  557. * Parses a PostgreSQL data_type string (as reported by
  558. * information_schema.columns) to a ColumnType.
  559. *
  560. * Matching is keyword-based, mirroring the SQLite implementation.
  561. *
  562. * @param type_str The PostgreSQL type string
  563. * @return The corresponding ColumnType
  564. */
  565. private ColumnType parse_postgres_type(string type_str) {
  566. var upper = type_str.up();
  567. if ("BIGINT" in upper || "INT8" in upper) {
  568. return ColumnType.INT_64;
  569. }
  570. if ("SMALLINT" in upper || "INTEGER" in upper || "INT4" in upper
  571. || "INT" == upper.strip() || "SERIAL" in upper) {
  572. return ColumnType.INT_32;
  573. }
  574. if ("UUID" in upper) {
  575. return ColumnType.UUID;
  576. }
  577. if ("BYTEA" in upper || "BLOB" in upper || "BINARY" in upper) {
  578. return ColumnType.BINARY;
  579. }
  580. if ("BOOLEAN" in upper || "BOOL" in upper) {
  581. return ColumnType.BOOLEAN;
  582. }
  583. if ("TIMESTAMP" in upper || "DATE" in upper || "TIME" in upper) {
  584. return ColumnType.DATETIME;
  585. }
  586. if ("DOUBLE" in upper || "REAL" in upper || "FLOAT" in upper
  587. || "NUMERIC" in upper || "DECIMAL" in upper) {
  588. return ColumnType.DECIMAL;
  589. }
  590. if ("CHAR" in upper || "CLOB" in upper || "TEXT" in upper
  591. || "JSON" in upper || "XML" in upper) {
  592. return ColumnType.TEXT;
  593. }
  594. // Default to TEXT
  595. return ColumnType.TEXT;
  596. }
  597. // === Projection query methods ===
  598. /**
  599. * Builds a SELECT statement with JOINs for a projection query.
  600. *
  601. * Constructs a complete SELECT from a ProjectionDefinition including
  602. * all JOINs, selections, WHERE/HAVING clauses, GROUP BY, ORDER BY,
  603. * LIMIT and OFFSET — all standard SQL on PostgreSQL.
  604. */
  605. public string build_projection_select(
  606. ProjectionDefinition definition,
  607. VariableTranslator translator,
  608. string? where_clause,
  609. string? having_clause,
  610. Vector<OrderByClause> order_by,
  611. int64? limit,
  612. int64? offset
  613. ) {
  614. var sql = new StringBuilder();
  615. sql.append("SELECT ");
  616. build_select_clause(sql, definition, translator);
  617. sql.append("\nFROM ");
  618. build_from_clause(sql, definition, translator);
  619. foreach (var join in definition.joins) {
  620. sql.append("\n");
  621. build_join_clause(sql, join, translator);
  622. }
  623. if (where_clause != null && where_clause.length > 0) {
  624. sql.append("\nWHERE ");
  625. sql.append(where_clause);
  626. }
  627. if (definition.group_by_expressions.length > 0) {
  628. sql.append("\nGROUP BY ");
  629. bool first = true;
  630. foreach (var group_expr in definition.group_by_expressions) {
  631. if (!first) {
  632. sql.append(", ");
  633. }
  634. first = false;
  635. sql.append(translator.translate_expression(group_expr));
  636. }
  637. }
  638. if (having_clause != null && having_clause.length > 0) {
  639. sql.append("\nHAVING ");
  640. sql.append(having_clause);
  641. }
  642. if (order_by.length > 0) {
  643. sql.append("\nORDER BY ");
  644. bool first = true;
  645. foreach (var order in order_by) {
  646. if (!first) {
  647. sql.append(", ");
  648. }
  649. first = false;
  650. sql.append(translator.translate_expression(order.expression));
  651. if (order.descending) {
  652. sql.append(" DESC");
  653. }
  654. }
  655. }
  656. if (limit != null) {
  657. sql.append("\nLIMIT ");
  658. sql.append(limit.to_string());
  659. }
  660. if (offset != null) {
  661. sql.append("\nOFFSET ");
  662. sql.append(offset.to_string());
  663. }
  664. return sql.str;
  665. }
  666. /**
  667. * Builds the SELECT clause with all selections.
  668. *
  669. * Each selection is formatted as: expression AS unique_alias.
  670. * Scalar selections and SCALAR-mode collections translate their
  671. * expressions; nested projections and non-scalar collections use
  672. * NULL placeholders, matching the SQLite behaviour.
  673. */
  674. private void build_select_clause(
  675. StringBuilder sql,
  676. ProjectionDefinition definition,
  677. VariableTranslator translator
  678. ) {
  679. bool first = true;
  680. int column_index = 0;
  681. foreach (var selection in definition.selections) {
  682. if (!first) {
  683. sql.append(", ");
  684. }
  685. first = false;
  686. column_index++;
  687. string column_alias = @"col_$(column_index)_$(definition.projection_type.name())_$(selection.friendly_name)";
  688. selection.column_alias = column_alias;
  689. var scalar_selection = selection as ScalarSelection<Object, Object>;
  690. if (scalar_selection != null) {
  691. string translated_expr = translator.translate_expression(scalar_selection.expression);
  692. sql.append(translated_expr);
  693. sql.append(" AS ");
  694. sql.append(column_alias);
  695. continue;
  696. }
  697. var collection_selection = selection as CollectionSelection;
  698. if (collection_selection != null && collection_selection.item_mode == CollectionItemMode.SCALAR) {
  699. string translated_expr = translator.translate_expression(collection_selection.entry_point_expression);
  700. sql.append(translated_expr);
  701. sql.append(" AS ");
  702. sql.append(column_alias);
  703. continue;
  704. }
  705. sql.append("NULL AS ");
  706. sql.append(column_alias);
  707. }
  708. }
  709. /**
  710. * Builds the FROM clause with the primary source.
  711. *
  712. * Format: table_name alias
  713. */
  714. private void build_from_clause(
  715. StringBuilder sql,
  716. ProjectionDefinition definition,
  717. VariableTranslator translator
  718. ) {
  719. if (definition.source == null) {
  720. return;
  721. }
  722. sql.append(definition.source.table_name);
  723. sql.append(" ");
  724. sql.append(translator.translate_variable(definition.source.variable_name));
  725. }
  726. /**
  727. * Builds a JOIN clause for a join definition.
  728. *
  729. * Format: LEFT JOIN table_name alias ON condition
  730. */
  731. private void build_join_clause(
  732. StringBuilder sql,
  733. JoinDefinition join,
  734. VariableTranslator translator
  735. ) {
  736. sql.append("LEFT JOIN ");
  737. sql.append(join.table_name);
  738. sql.append(" ");
  739. sql.append(translator.translate_variable(join.variable_name));
  740. sql.append(" ON ");
  741. sql.append(translator.translate_expression(join.join_condition));
  742. }
  743. /**
  744. * Builds a subquery wrapper for mixed aggregate/non-aggregate OR
  745. * conditions: SELECT * FROM (inner) subq WHERE combined_where.
  746. */
  747. public string wrap_subquery_for_mixed_or(
  748. string inner_query,
  749. string combined_where
  750. ) {
  751. return @"SELECT * FROM ($inner_query) subq WHERE $combined_where";
  752. }
  753. /**
  754. * Generates a table alias with type information for debugging.
  755. *
  756. * Format: val_N_TypeName (e.g., val_1_User, val_2_Order)
  757. */
  758. public string generate_table_alias(int index, string type_name) {
  759. return "val_%d_%s".printf(index, type_name);
  760. }
  761. }
  762. }