Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Ruby Kaigi 2025 - Parsing and generating SQLite...

Ruby Kaigi 2025 - Parsing and generating SQLite's SQL dialect with Ruby

SQLite's popularity is on the rise, and likewise the ecosystem of tools around it is growing. Unfortunately, SQLite does not expose its parser for 3rd parties to use independently. This greatly limits the ability of developers to build tools that must interact with SQLite's SQL dialect. And so, I have hand-written a 100% Ruby, 100% compatible parser for SQLite's SQL dialect. In addition, having a complete AST permits us to also generate SQL queries from terse, structured Ruby code. In this talk, I will demonstrate how we ensure that the parser is 100% compatible with SQLite's SQL dialect. We will also explore how the parser is implemented and what kind of AST it produces. Then, we will dive into how to use the parser to build tools that can analyze and manipulate SQL queries. Finally, we will look at how to use the generator to build tools that can generate SQL queries programmatically. As Ruby's only full SQLite SQL parser, this library opens up a world of possibilities for developers

Avatar for Stephen

Stephen

May 26, 2025
Tweet

More Decks by Stephen

Other Decks in Programming

Transcript

  1. 0

  2. Why

  3. CREATE TABLE table ( id INTEGER PRIMARY KEY AUTOINCREMENT, foo

    TEXT, UNIQUE (id, foo) CHECK (LENGTH(foo) < 10) )
  4. CREATE TABLE table ( id INTEGER PRIMARY KEY AUTOINCREMENT, foo

    TEXT, UNIQUE (id, foo) CHECK (LENGTH(foo) < 10) )
  5. CREATE TABLE table ( id INTEGER PRIMARY KEY AUTOINCREMENT, foo

    TEXT, UNIQUE (id, foo) CHECK (LENGTH(foo) < 10) ) ,
  6. 0

  7. 0

  8. create_table :posts, force: true do |t| t.json :payload, null: false,

    default: {} t.virtual :external_id, type: :string, as: "JSON_EXTRACT(payload, '$.id')", stored: true, null: false, index: true end
  9. create_table :posts, force: true do |t| t.json :payload, null: false,

    default: {} t.virtual :external_id, type: :string, as: "JSON_EXTRACT(payload, '$.id')", stored: true, null: false, index: true end
  10. # Splitting with left parentheses and discarding the first part

    will return all columns separated with comma(,). result.partition(UNQUOTED_OPEN_PARENS_REGEX) .last .sub(FINAL_CLOSE_PARENS_REGEX, "") .split(",") .map(&:strip)
  11. # Splitting with left parentheses and discarding the first part

    will return all columns separated with comma(,). result.partition(UNQUOTED_OPEN_PARENS_REGEX) .last .sub(FINAL_CLOSE_PARENS_REGEX, "") .split(/ ,(?=\s(?:CONSTRAINT|"(?:#{Regexp.union(column_names).source})")) /i).map(&:strip)
  12. 0

  13. Why Foundation for next wave of SQLite tooling Secure Rails’

    foundation for introspecting SQLite (?) Challenge myself
  14. How

  15. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  16. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  17. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  18. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  19. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  20. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  21. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  22. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  23. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  24. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  25. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  26. create temp table if not exists `schema0`."tb0" ( [c], c0

    integer primary key desc on conflict abort autoincrement, c1 int constraint 'nonnull' not null on conflict rollback, c2 text constraint 'uniqued' unique on conflict ignore, c3 blob constraint 'checked' check (c3 > 0), c4 real constraint 'defaulted' default (1.1 * 2.2), c5 any constraint 'collated' collate rtrim, c6 decimal(4, 6) generated always as (1.1 + 2.2) stored, c7 constraint 'fk0' references tb1(c1) on delete set null on update cascade match full deferrable initially deferred, primary key (c0, c1 autoincrement) on conflict abort, unique (c0, c1) on conflict rollback check (c0 > 0) foreign key (c0) references tb1(c1) on delete cascade on update restrict match full deferrable initially deferred ) STRICT, WITHOUT ROWID;
  27. Plume::CreateTableStatement( temporary = true, if_not_exists = true, table = Plume::TableName(schema

    = "schema0", table = "tb0"), columns = [ Plume::ColumnDefinition(name = "c"), Plume::ColumnDefinition( name = "c0", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint( name = "pk0", direction = :DESC, autoincrement = true, conflict_clause = Plume::ConflictClause(resolution = :ABORT), ), ], ), Plume::ColumnDefinition( name = "c1", type = Plume::ColumnType(text = "int", affinity = :INTEGER),
  28. Patterson’s Parsing Principles write Ruby parser to minimize Ruby <—>

    C calls minimize allocations (pass integers and symbols) use lazy allocations when possible (e.g. token values)
  29. class Plume::Lexer def initialize(sql) @sql = sql.freeze @cursor, @anchor =

    0 end def next_token @anchor = @cursor lex_next_token end def value = @sql.byteslice(@anchor, (@cursor - @anchor)) def lex_next_token = # ... def peek(n = 0) = @sql.getbyte(@cursor+n) def step(n = 1) = @cursor += n def scan(n = 0) = peek(n).tap { step(n+1) } end
  30. class Plume::Lexer def initialize(sql) @sql = sql.freeze @cursor, @anchor =

    0 end def next_token @anchor = @cursor lex_next_token end def value = @sql.byteslice(@anchor, (@cursor - @anchor)) def lex_next_token = # ... def peek(n = 0) = @sql.getbyte(@cursor+n) def step(n = 1) = @cursor += n def scan(n = 0) = peek(n).tap { step(n+1) } end
  31. class Plume::Lexer def initialize(sql) @sql = sql.freeze @cursor, @anchor =

    0 end def next_token @anchor = @cursor lex_next_token end def value = @sql.byteslice(@anchor, (@cursor - @anchor)) def lex_next_token = # ... def peek(n = 0) = @sql.getbyte(@cursor+n) def step(n = 1) = @cursor += n def scan(n = 0) = peek(n).tap { step(n+1) } end
  32. class Plume::Lexer def initialize(sql) @sql = sql.freeze @cursor, @anchor =

    0 end def next_token @anchor = @cursor lex_next_token end def value = @sql.byteslice(@anchor, (@cursor - @anchor)) def lex_next_token = # ... def peek(n = 0) = @sql.getbyte(@cursor+n) def step(n = 1) = @cursor += n def scan(n = 0) = peek(n).tap { step(n+1) } end
  33. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end def create_table_stmt = # ... private def require(token) = # ... def maybe(token = nil, &block) = # ... def require_all_of(*tokens) = # ... def maybe_all_of(*tokens) = # ... def maybe_one_of(*tokens) = # ... def require_one_of(*tokens) = # ... def require_some(**options, &block) = # ... def maybe_some(**options, &block) = # ... end
  34. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end def create_table_stmt = # ... private def require(token) = # ... def maybe(token = nil, &block) = # ... def require_all_of(*tokens) = # ... def maybe_all_of(*tokens) = # ... def maybe_one_of(*tokens) = # ... def require_one_of(*tokens) = # ... def require_some(**options, &block) = # ... def maybe_some(**options, &block) = # ... end
  35. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end # ... def create_table_stmt create_kw = require :CREATE temp_kw = maybe_one_of :TEMP, :TEMPORARY table_kw = require :TABLE if_not_exists_kw = maybe_all_of :IF, :NOT, :EXISTS table_name = table_name() # ... end end
  36. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end # ... def create_table_stmt create_kw = require :CREATE temp_kw = maybe_one_of :TEMP, :TEMPORARY table_kw = require :TABLE if_not_exists_kw = maybe_all_of :IF, :NOT, :EXISTS table_name = table_name() # ... end end
  37. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end # ... def create_table_stmt create_kw = require :CREATE temp_kw = maybe_one_of :TEMP, :TEMPORARY table_kw = require :TABLE if_not_exists_kw = maybe_all_of :IF, :NOT, :EXISTS table_name = table_name() # ... end end
  38. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end # ... def create_table_stmt create_kw = require :CREATE temp_kw = maybe_one_of :TEMP, :TEMPORARY table_kw = require :TABLE if_not_exists_kw = maybe_all_of :IF, :NOT, :EXISTS table_name = table_name() # ... end end
  39. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end # ... def create_table_stmt create_kw = require :CREATE temp_kw = maybe_one_of :TEMP, :TEMPORARY table_kw = require :TABLE if_not_exists_kw = maybe_all_of :IF, :NOT, :EXISTS table_name = table_name() # ... end end
  40. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end # ... def create_table_stmt create_kw = require :CREATE temp_kw = maybe_one_of :TEMP, :TEMPORARY table_kw = require :TABLE if_not_exists_kw = maybe_all_of :IF, :NOT, :EXISTS table_name = table_name() # ... end end
  41. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end # ... def create_table_stmt create_kw = require :CREATE temp_kw = maybe_one_of :TEMP, :TEMPORARY table_kw = require :TABLE if_not_exists_kw = maybe_all_of :IF, :NOT, :EXISTS table_name = require { table_name() } # ... end end
  42. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end # ... def create_table_stmt create_kw = require :CREATE temp_kw = maybe_one_of :TEMP, :TEMPORARY table_kw = require :TABLE if_not_exists_kw = maybe_all_of :IF, :NOT, :EXISTS table_name = table_name() # ... end end
  43. class Plume::Parser def initialize(sql) @lexer = Lexer.new(sql, skip_spaces: true) @peek_buffer

    = [] end # ... def create_table_stmt create_kw = require :CREATE temp_kw = maybe_one_of :TEMP, :TEMPORARY table_kw = require :TABLE if_not_exists_kw = maybe_all_of :IF, :NOT, :EXISTS table_name = table_name() # ... end end
  44. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  45. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  46. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  47. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  48. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  49. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  50. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  51. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  52. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  53. CREATE TABLE table ( id INTEGER PRIMARY KEY AUTOINCREMENT, foo

    TEXT, UNIQUE (id, foo) CHECK (LENGTH(foo) < 10) ) ,
  54. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  55. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  56. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  57. class Plume::Parser def create_table_stmt # ... if maybe :AS #

    ... elsif (columns_lp = maybe :LP) columns = require_some(trailing_sep: :COMMA) { column_def } constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do # ... end columns_rp = require :RP options = maybe_some(trailing_sep: :COMMA) do # ... end CreateTableStatement.new(...) end end end
  58. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  59. Plume::CreateTableStatement( full_source = "create table users (id integer primary key

    autoincrement, email string not null unique)", create_kw = [:CREATE, 0, 6, :keyword], table_kw = [:TABLE, 7, 12, :keyword], columns_lp = [:LP, 19, 20, :punctuation], columns_rp = [:RP, 93, 94, :punctuation], table = Plume::TableName( full_source = "...", table_tk=[:ID, 13, 18, :identifier] ), columns=[ Plume::ColumnDefinition( full_source = "...", trailing = [:COMMA, 59, 60, :punctuation], name_tk = [:ID, 23, 25, :identifier], type = Plume::ColumnType( full_source = "...", text_span = [[], 26, 33, :keyword] ), constraints = [
  60. Plume::CreateTableStatement( full_source = "create table users (id integer primary key

    autoincrement, email string not null unique)", create_kw = [:CREATE, 0, 6, :keyword], table_kw = [:TABLE, 7, 12, :keyword], columns_lp = [:LP, 19, 20, :punctuation], columns_rp = [:RP, 93, 94, :punctuation], table = Plume::TableName( full_source = "...", table_tk=[:ID, 13, 18, :identifier] ), columns=[ Plume::ColumnDefinition( full_source = "...", trailing = [:COMMA, 59, 60, :punctuation], name_tk = [:ID, 23, 25, :identifier], type = Plume::ColumnType( full_source = "...", text_span = [[], 26, 33, :keyword] ), constraints = [
  61. Plume::CreateTableStatement( full_source = "create table users (id integer primary key

    autoincrement, email string not null unique)", create_kw = [:CREATE, 0, 6, :keyword], table_kw = [:TABLE, 7, 12, :keyword], columns_lp = [:LP, 19, 20, :punctuation], columns_rp = [:RP, 93, 94, :punctuation], table = Plume::TableName( full_source = "...", table_tk=[:ID, 13, 18, :identifier] ), columns=[ Plume::ColumnDefinition( full_source = "...", trailing = [:COMMA, 59, 60, :punctuation], name_tk = [:ID, 23, 25, :identifier], type = Plume::ColumnType( full_source = "...", text_span = [[], 26, 33, :keyword] ), constraints = [
  62. Plume::CreateTableStatement( full_source = "create table users (id integer primary key

    autoincrement, email string not null unique)", create_kw = [:CREATE, 0, 6, :keyword], table_kw = [:TABLE, 7, 12, :keyword], columns_lp = [:LP, 19, 20, :punctuation], columns_rp = [:RP, 93, 94, :punctuation], table = Plume::TableName( full_source = "...", table_tk=[:ID, 13, 18, :identifier] ), columns=[ Plume::ColumnDefinition( full_source = "...", trailing = [:COMMA, 59, 60, :punctuation], name_tk = [:ID, 23, 25, :identifier], type = Plume::ColumnType( full_source = "...", text_span = [[], 26, 33, :keyword] ), constraints = [
  63. Plume::CreateTableStatement( full_source = "create table users (id integer primary key

    autoincrement, email string not null unique)", create_kw = [:CREATE, 0, 6, :keyword], table_kw = [:TABLE, 7, 12, :keyword], columns_lp = [:LP, 19, 20, :punctuation], columns_rp = [:RP, 93, 94, :punctuation], table = Plume::TableName( full_source = "...", table_tk=[:ID, 13, 18, :identifier] ), columns=[ Plume::ColumnDefinition( full_source = "...", trailing = [:COMMA, 59, 60, :punctuation], name_tk = [:ID, 23, 25, :identifier], type = Plume::ColumnType( full_source = "...", text_span = [[], 26, 33, :keyword] ), constraints = [
  64. Plume::CreateTableStatement( full_source = "create table users (id integer primary key

    autoincrement, email string not null unique)", create_kw = [:CREATE, 0, 6, :keyword], table_kw = [:TABLE, 7, 12, :keyword], columns_lp = [:LP, 19, 20, :punctuation], columns_rp = [:RP, 93, 94, :punctuation], table = Plume::TableName( full_source = "...", table_tk=[:ID, 13, 18, :identifier] ), columns=[ Plume::ColumnDefinition( full_source = "...", trailing = [:COMMA, 59, 60, :punctuation], name_tk = [:ID, 23, 25, :identifier], type = Plume::ColumnType( full_source = "...", text_span = [[], 26, 33, :keyword] ), constraints = [
  65. Plume::CreateTableStatement( full_source = "create table users (id integer primary key

    autoincrement, email string not null unique)", create_kw = [:CREATE, 0, 6, :keyword], table_kw = [:TABLE, 7, 12, :keyword], columns_lp = [:LP, 19, 20, :punctuation], columns_rp = [:RP, 93, 94, :punctuation], table = Plume::TableName( full_source = "...", table_tk=[:ID, 13, 18, :identifier] ), columns=[ Plume::ColumnDefinition( full_source = "...", trailing = [:COMMA, 59, 60, :punctuation], name_tk = [:ID, 23, 25, :identifier], type = Plume::ColumnType( full_source = "...", text_span = [[], 26, 33, :keyword] ), constraints = [
  66. Plume::CreateTableStatement( table = Plume::TableName(table = "users"), columns = [ Plume::ColumnDefinition(

    name = "id", type = Plume::ColumnType(text = "integer", affinity = :INTEGER), constraints = [ Plume::PrimaryKeyColumnConstraint(autoincrement = true), ], ), Plume::ColumnDefinition( name = "email", type = Plume::ColumnType(text = "string", affinity = :ANY), constraints = [ Plume::NotNullColumnConstraint(), Plume::UniqueColumnConstraint(), ] ) ] )
  67. How hand-written recursive descent parser minimal allocations, parsing token by

    token grammar encoded with helper methods concrete syntax tree with abstract representation
  68. { SELECT: ALL, FROM: Artists, WHERE: { EXISTS: { SELECT:

    true, FROM: { JSON_EACH: Artists.skills }, WHERE: { value: ['skill_1', 'skill_2'] }, LIMIT: 1 } }, ORDER_BY: { Artists.id => ["a".."z", nil] } }
  69. What next More statements Bug hunt for incompatibilities Web AST

    explorer SQL generation … your contribution?
  70. 0