From c550df16a9381e2ec3168b3e4ecd629840019d07 Mon Sep 17 00:00:00 2001 From: Stephane Nolin Date: Fri, 9 Jan 2026 20:24:24 -0500 Subject: [PATCH 1/2] Add support for GENERATED column constraint --- debug/main.c | 1 + debug/sql3parse_debug.c | 16 +++++++++++++ sql3parse_table.c | 51 +++++++++++++++++++++++++++++++++++++++-- sql3parse_table.h | 8 +++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/debug/main.c b/debug/main.c index ffb4a85..f57b2fe 100644 --- a/debug/main.c +++ b/debug/main.c @@ -54,6 +54,7 @@ int main (void) { , test -- test 123\n\ INTEGER, UNIQUE (flags /* Hello World*/, test) -- This is another table comment\n\ );"); + test_case("CREATE TABLE Sales(Price INT, Qty INT, Total INT GENERATED ALWAYS AS (Price*Qty) VIRTUAL, Item TEXT)"); // https://www.sqlite.org/lang_altertable.html test_case("ALTER TABLE foo RENAME TO bar"); diff --git a/debug/sql3parse_debug.c b/debug/sql3parse_debug.c index 93831f4..b134323 100644 --- a/debug/sql3parse_debug.c +++ b/debug/sql3parse_debug.c @@ -51,6 +51,14 @@ static const char *sql3constraint_type_str (sql3constraint_type type) { } } +static const char *sql3generated_type_str (sql3gen_type type) { + switch (type) { + case SQL3GENTYPE_NONE: return "NONE"; + case SQL3GENTYPE_STORED: return "STORED"; + case SQL3GENTYPE_VIRTUAL: return "VIRTUAL"; + } +} + //MARK: - Dump Code - static void sql3string_dump (sql3string *ptr, const char *label) { @@ -148,6 +156,10 @@ static void sql3column_dump (sql3column *column) { clause = sql3column_unique_conflictclause(column); if (clause != SQL3CONFLICT_NONE) printf("Unique Conflict Cause: %s\n", sql3conflict_clause_str(clause)); + + sql3gen_type gen_type = sql3column_generated_type(column); + if (gen_type != SQL3GENTYPE_NONE) + printf("Generated Type: %s\n", sql3generated_type_str(gen_type)); // check expr ptr = sql3column_check_expr(column); @@ -164,6 +176,10 @@ static void sql3column_dump (sql3column *column) { // foreign key sql3foreignkey *fk = sql3column_foreignkey_clause(column); sql3foreignkey_dump(fk); + + // generated expr + ptr = sql3column_generated_expr(column); + sql3string_dump(ptr, "Generated Expression"); } static void sql3tableconstraint_dump (sql3tableconstraint *constraint) { diff --git a/sql3parse_table.c b/sql3parse_table.c index 7b75ee8..2c133f2 100644 --- a/sql3parse_table.c +++ b/sql3parse_table.c @@ -25,7 +25,8 @@ typedef enum { // constraints TOK_CONSTRAINT, TOK_PRIMARY, TOK_KEY, TOK_UNIQUE, TOK_CHECK, TOK_FOREIGN, TOK_ON, TOK_CONFLICT, TOK_ROLLBACK, TOK_ABORT, TOK_FAIL, TOK_IGNORE, TOK_REPLACE, - TOK_COLLATE, TOK_ASC, TOK_DESC, TOK_AUTOINCREMENT, + TOK_COLLATE, TOK_ASC, TOK_DESC, TOK_AUTOINCREMENT, TOK_GENERATED, TOK_ALWAYS, + TOK_STORED, TOK_VIRTUAL, // foreign key clause TOK_REFERENCES, TOK_DELETE, TOK_UPDATE, TOK_SET, TOK_NULL, TOK_DEFAULT, TOK_CASCADE, @@ -70,6 +71,8 @@ struct sql3column { sql3string default_expr; // default expression (can be NULL) sql3string collate_name; // collate name (can be NULL) sql3foreignkey *foreignkey_clause; // foreign key clause (can be NULL) + sql3string generated_expr; // generated expression (can be NULL) + sql3gen_type generated_type; // generated type }; struct sql3tableconstraint { @@ -224,7 +227,8 @@ static bool symbol_is_punctuation (sql3char c) { static bool token_is_column_constraint (sql3token_t t) { return ((t == TOK_CONSTRAINT) || (t == TOK_PRIMARY) || (t == TOK_NOT) || (t == TOK_UNIQUE) || - (t == TOK_CHECK) || (t == TOK_DEFAULT) || (t == TOK_COLLATE) || (t == TOK_REFERENCES)); + (t == TOK_CHECK) || (t == TOK_DEFAULT) || (t == TOK_COLLATE) || (t == TOK_REFERENCES) || + (t == TOK_GENERATED) || (t == TOK_AS)); } static bool token_is_table_constraint (sql3token_t t) { @@ -282,6 +286,8 @@ static sql3token_t sql3lexer_keyword (const char *ptr, size_t length) { if (str_nocasencmp(ptr, "strict", length) == 0) return TOK_STRICT; if (str_nocasencmp(ptr, "rename", length) == 0) return TOK_RENAME; if (str_nocasencmp(ptr, "column", length) == 0) return TOK_COLUMN; + if (str_nocasencmp(ptr, "always", length) == 0) return TOK_ALWAYS; + if (str_nocasencmp(ptr, "stored", length) == 0) return TOK_STORED; break; case 7: @@ -292,6 +298,7 @@ static sql3token_t sql3lexer_keyword (const char *ptr, size_t length) { if (str_nocasencmp(ptr, "replace", length) == 0) return TOK_REPLACE; if (str_nocasencmp(ptr, "cascade", length) == 0) return TOK_CASCADE; if (str_nocasencmp(ptr, "foreign", length) == 0) return TOK_FOREIGN; + if (str_nocasencmp(ptr, "virtual", length) == 0) return TOK_VIRTUAL; break; case 8: @@ -305,6 +312,7 @@ static sql3token_t sql3lexer_keyword (const char *ptr, size_t length) { if (str_nocasencmp(ptr, "temporary", length) == 0) return TOK_TEMP; if (str_nocasencmp(ptr, "initially", length) == 0) return TOK_INITIALLY; if (str_nocasencmp(ptr, "immediate", length) == 0) return TOK_IMMEDIATE; + if (str_nocasencmp(ptr, "generated", length) == 0) return TOK_GENERATED; break; case 10: @@ -503,6 +511,19 @@ static sql3error_code sql3parse_optionalconflitclause (sql3state *state, sql3con return SQL3ERROR_NONE; } +static sql3error_code sql3parse_optionalgentype(sql3state* state, sql3gen_type* type) { + sql3token_t token = sql3lexer_peek(state); + *type = SQL3GENTYPE_NONE; + + if ((token == TOK_STORED) || (token == TOK_VIRTUAL)) { + sql3lexer_next(state); // consume token + if (token == TOK_STORED) *type = SQL3GENTYPE_STORED; + else *type = SQL3GENTYPE_VIRTUAL; + } + + return SQL3ERROR_NONE; +} + static void sql3free_foreignkey(sql3foreignkey *fk) { if (!fk) return; // shallow slices only; nothing to free inside currently @@ -953,6 +974,23 @@ static sql3error_code sql3parse_column_constraints (sql3state *state, sql3column if (!fk) return SQL3ERROR_SYNTAX; column->foreignkey_clause = fk; } break; + + case TOK_GENERATED: + case TOK_AS: { + if (token == TOK_GENERATED) { + token = sql3lexer_next(state); + // 'column-constraint' syntax indicates that TOK_ALWAYS is + // mandatory but SQLite accepts the statement without it. + if (token == TOK_ALWAYS) token = sql3lexer_next(state); + if (token != TOK_AS) return SQL3ERROR_SYNTAX; + } + token = sql3lexer_peek(state); + if (token != TOK_OPEN_PARENTHESIS) return SQL3ERROR_SYNTAX; + + // expressions are extracted (not fully parsed) in this version + column->generated_expr = sql3parse_expression(state); + if (sql3parse_optionalgentype(state, &column->generated_type) != SQL3ERROR_NONE) return SQL3ERROR_SYNTAX; + } break; default: return SQL3ERROR_SYNTAX; @@ -1505,6 +1543,15 @@ sql3foreignkey *sql3column_foreignkey_clause (sql3column *column) { return column->foreignkey_clause; } +sql3string* sql3column_generated_expr(sql3column* column) { + CHECK_STR(column->generated_expr); + return &column->generated_expr; +} + +sql3gen_type sql3column_generated_type(sql3column* column) { + return column->generated_type; +} + // MARK: - Public Foreign Key Functions - sql3string *sql3foreignkey_table (sql3foreignkey *fk) { diff --git a/sql3parse_table.h b/sql3parse_table.h index 741ade6..9ad9393 100644 --- a/sql3parse_table.h +++ b/sql3parse_table.h @@ -97,6 +97,12 @@ typedef enum { SQL3ALTER_DROP_COLUMN } sql3statement_type; +typedef enum { + SQL3GENTYPE_NONE, + SQL3GENTYPE_STORED, + SQL3GENTYPE_VIRTUAL +} sql3gen_type; + // Main http://www.sqlite.org/lang_createtable.html sql3table *sql3parse_table (const char *sql, size_t length, sql3error_code *error); @@ -147,6 +153,8 @@ sql3string *sql3column_check_expr (sql3column *column); sql3string *sql3column_default_expr (sql3column *column); sql3string *sql3column_collate_name (sql3column *column); sql3foreignkey *sql3column_foreignkey_clause (sql3column *column); +sql3string* sql3column_generated_expr(sql3column* column); +sql3gen_type sql3column_generated_type(sql3column* column); // Foreign Key sql3string *sql3foreignkey_table (sql3foreignkey *fk); From d082155d7ada0aa947f7f8c77dd794c98737dd6b Mon Sep 17 00:00:00 2001 From: Stephane Nolin Date: Sun, 11 Jan 2026 09:34:45 -0500 Subject: [PATCH 2/2] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 71aa4f4..3e7725b 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ sql3string *sql3column_check_expr (sql3column *column); sql3string *sql3column_default_expr (sql3column *column); sql3string *sql3column_collate_name (sql3column *column); sql3foreignkey *sql3column_foreignkey_clause (sql3column *column); +sql3string *sql3column_generated_expr (sql3column* column); +sql3gen_type sql3column_generated_type (sql3column* column); // Foreign key sql3string *sql3foreignkey_table (sql3foreignkey *fk);