···44import gleam/dict.{type Dict}
55import gleam/int
66import gleam/list
77-import gleam/option.{None, Some}
77+import gleam/option.{type Option, None, Some}
88import gleam/result
99import gleam/set.{type Set}
1010import gleam/string
1111+import justin
1112import non_empty_list.{type NonEmptyList}
1213import simplifile
1414+import splitter.{type Splitter}
1315import squirrel/internal/error.{
1416 type Error, CannotReadFile, QueryFileHasInvalidName,
1517 QueryReturnsMultipleValuesWithTheSameName,
···4951 name: gleam.ValueIdentifier,
5052 comment: List(String),
5153 content: String,
5252- params: List(gleam.Type),
5454+ params: List(Parameter),
5355 returns: List(gleam.Field),
5656+ )
5757+}
5858+5959+pub type Parameter {
6060+ Parameter(
6161+ /// The name inferred for this parameter.
6262+ name: Option(gleam.ValueIdentifier),
6363+ type_: gleam.Type,
5464 )
5565}
5666···6272 returns returns: List(gleam.Field),
6373) -> Result(TypedQuery, Error) {
6474 let UntypedQuery(file:, name:, comment:, content:, starting_line:) = query
6565-7575+ let params = infer_parameter_names(query, params)
6676 case duplicate_names(returns) {
6777 [] ->
6878 Ok(TypedQuery(
···394404395405 let CodeGenState(imports:, needs_uuid_decoder:, enums:) = state
396406397397- let utils =
398398- []
399399- |> prepend_if(needs_uuid_decoder, doc.from_string(uuid_decoder))
407407+ let utils = case needs_uuid_decoder {
408408+ True -> [doc.from_string(uuid_decoder)]
409409+ False -> []
410410+ }
400411401412 // We always want to output the imports and the code for the queries.
402413 // But in case we also need some helpers we add a final section to our file
···515526 let acc = #(state, [], [])
516527 use #(state, args, encoders), param, i <- list.index_fold(params, acc)
517528518518- let arg = "arg_" <> int.to_string(i + 1)
529529+ let arg = generate_argument_name(param, i)
519530 let #(state, arg_type) =
520520- gleam_type_to_field_type(state, param, FunctionArgument)
521521- let #(state, encoder) = gleam_type_to_encoder(state, param, arg)
531531+ gleam_type_to_field_type(state, param.type_, FunctionArgument)
532532+ let #(state, encoder) = gleam_type_to_encoder(state, param.type_, arg)
522533523534 let arg = doc.concat([doc.from_string(arg <> ": "), arg_type])
524535 #(state, [arg, ..args], [encoder, ..encoders])
···553564 #(state, code)
554565}
555566567567+fn generate_argument_name(param: Parameter, position: Int) -> String {
568568+ let name = case param.name {
569569+ Some(name) -> gleam.value_identifier_to_string(name)
570570+ None -> "arg_" <> int.to_string(position + 1)
571571+ }
572572+573573+ // There's some names we don't want to use, and if some identifier is a
574574+ // keyword.
575575+ case name {
576576+ "true" | "false" | "null" -> "arg_" <> int.to_string(position + 1)
577577+ _ -> name
578578+ }
579579+ // We need to rename things to avoid conflicting with stuff that exists in
580580+ // this scope!
581581+}
582582+556583fn pipe_all_encoders(doc: Document, decoders: List(Document)) -> Document {
557584 use doc, decoder <- list.fold(over: decoders, from: doc)
558585 doc |> pipe_call_doc("pog.parameter", _, [decoder])
···1010103710111038/// A comma separated list of items with some given open and closed delimiters.
10121039///
10131013-fn comma_list(open: String, content: List(Document), close: String) -> Document {
10401040+fn comma_list(
10411041+ open: String,
10421042+ content: List(Document),
10431043+ close: String,
10441044+) -> Document {
10141045 case content {
10151046 [] -> doc.from_string(open <> close)
10161047 _ ->
···10761107 })
10771108}
1078110910791079-// --- MISC UTILS --------------------------------------------------------------
11101110+// --- SQL QUERY PARAMETERS ANALYSIS -------------------------------------------
11111111+// This code analyses SQL queries to try and understand the names of the query
11121112+// parameters used in each.
11131113+11141114+type Context {
11151115+ NoIdentifier
11161116+11171117+ // We've found `$1 ...`
11181118+ QueryParameter(Int)
11191119+ // We've found `$1 = ...`
11201120+ QueryParameterEquality(Int)
11211121+11221122+ // We've found `wibble ...`
11231123+ Identifier(Name)
11241124+ // We've found `wibble = ...`
11251125+ IdentifierEquality(Name)
11261126+}
11271127+11281128+type Name {
11291129+ Column(String)
11301130+ TableAndColumn(String, String)
11311131+}
11321132+11331133+type Splitters {
11341134+ Splitters(whitespace: Splitter, comments: Splitter, strings: Splitter)
11351135+}
11361136+11371137+fn infer_parameter_names(
11381138+ query: UntypedQuery,
11391139+ params: List(gleam.Type),
11401140+) -> List(Parameter) {
11411141+ let names = parameter_names(query.content)
11421142+ list.index_map(params, fn(type_, i) {
11431143+ case dict.get(names, i + 1) {
11441144+ Ok(name) -> Parameter(name: Some(name), type_:)
11451145+ Error(_) -> Parameter(name: None, type_:)
11461146+ }
11471147+ })
11481148+}
11491149+11501150+fn parameter_names(query: String) -> Dict(Int, gleam.ValueIdentifier) {
11511151+ let query = string.lowercase(query)
11521152+ let splitters =
11531153+ Splitters(
11541154+ whitespace: splitter.new([" ", "\n", "\t", "\r"]),
11551155+ comments: splitter.new(["/*", "*/"]),
11561156+ strings: splitter.new(["''", "\\'", "'"]),
11571157+ )
11581158+11591159+ parameter_names_loop(splitters, query, NoIdentifier, dict.new())
11601160+ |> dict.fold(dict.new(), fn(acc, parameter, names) {
11611161+ case pick_name(names) {
11621162+ Ok(name) -> dict.insert(acc, parameter, name)
11631163+ Error(_) -> acc
11641164+ }
11651165+ })
11661166+}
11671167+11681168+fn pick_name(names: List(Name)) -> Result(gleam.ValueIdentifier, Nil) {
11691169+ list.find_map(list.reverse(names), fn(name) {
11701170+ let name = case name {
11711171+ Column(name) -> justin.snake_case(name)
11721172+ TableAndColumn(table, column) -> {
11731173+ let column = string.remove_prefix(table, from: column)
11741174+ justin.snake_case(table) <> "_" <> justin.snake_case(column)
11751175+ }
11761176+ }
11771177+ gleam.value_identifier(name)
11781178+ })
11791179+}
11801180+11811181+fn add_name(
11821182+ names: Dict(a, List(b)),
11831183+ parameter: a,
11841184+ name: b,
11851185+) -> Dict(a, List(b)) {
11861186+ dict.upsert(names, parameter, fn(existing) {
11871187+ case existing {
11881188+ Some(existing) -> [name, ..existing]
11891189+ None -> [name]
11901190+ }
11911191+ })
11921192+}
11931193+11941194+fn parameter_names_loop(
11951195+ splitters: Splitters,
11961196+ query: String,
11971197+ context: Context,
11981198+ names: Dict(Int, List(Name)),
11991199+) -> Dict(Int, List(Name)) {
12001200+ case query {
12011201+ // COMMENTS
12021202+ // We strip those out totally ignoring their content.
12031203+ // A comment also won't change the current context.
12041204+ "/*" <> query -> multiline_comment_loop(splitters, query, context, 1, names)
12051205+ "--" <> query ->
12061206+ case string.split_once(query, on: "\n") {
12071207+ Error(_) -> names
12081208+ Ok(#(_comment, query)) ->
12091209+ parameter_names_loop(splitters, query, context, names)
12101210+ }
12111211+12121212+ // STRINGS
12131213+ // We strip those out ignoring their content.
12141214+ "'" <> query -> string_loop(splitters, query, names)
12151215+12161216+ // WHITESPACE
12171217+ // Whitespace is ignored and we just move forward.
12181218+ // It also doesn't change the current context.
12191219+ " " <> query | "\n" <> query | "\r" <> query | "\t" <> query ->
12201220+ parameter_names_loop(splitters, query, context, names)
12211221+12221222+ // QUOTED IDENTIFIERS
12231223+ // Any sequence of characters in between double quotes.
12241224+ "\"" <> query ->
12251225+ case string.split_once(query, "\"") {
12261226+ // The identifier is not closed, this is a syntax error, we just return
12271227+ // all the names we've figured out so far
12281228+ Error(_) -> names
12291229+ // This could be a dotted identifier!!
12301230+ Ok(#(identifier, "." <> query)) ->
12311231+ dotted_identifier(splitters, query, context, identifier, names)
12321232+ // We've found an identifier, and so update the names and context
12331233+ // accordingly.
12341234+ Ok(#(identifier, query)) ->
12351235+ add_identifier(splitters, query, Column(identifier), context, names)
12361236+ }
12371237+12381238+ // IDENTIFIERS/KEYWORDS
12391239+ // If we see the start of an identifier we're gonna have to parse the whole
12401240+ // word and then keep going to see if it is the name of a query parameter.
12411241+ "a" as letter <> query
12421242+ | "b" as letter <> query
12431243+ | "c" as letter <> query
12441244+ | "d" as letter <> query
12451245+ | "e" as letter <> query
12461246+ | "f" as letter <> query
12471247+ | "g" as letter <> query
12481248+ | "h" as letter <> query
12491249+ | "i" as letter <> query
12501250+ | "j" as letter <> query
12511251+ | "k" as letter <> query
12521252+ | "l" as letter <> query
12531253+ | "m" as letter <> query
12541254+ | "n" as letter <> query
12551255+ | "o" as letter <> query
12561256+ | "p" as letter <> query
12571257+ | "q" as letter <> query
12581258+ | "r" as letter <> query
12591259+ | "s" as letter <> query
12601260+ | "t" as letter <> query
12611261+ | "u" as letter <> query
12621262+ | "v" as letter <> query
12631263+ | "w" as letter <> query
12641264+ | "x" as letter <> query
12651265+ | "y" as letter <> query
12661266+ | "z" as letter <> query
12671267+ | "_" as letter <> query ->
12681268+ identifier_loop(splitters, query, letter, context, names)
12691269+12701270+ // QUERY PARAMETERS
12711271+ "$1" <> query -> parameter_loop(splitters, query, context, 1, names)
12721272+ "$2" <> query -> parameter_loop(splitters, query, context, 2, names)
12731273+ "$3" <> query -> parameter_loop(splitters, query, context, 3, names)
12741274+ "$4" <> query -> parameter_loop(splitters, query, context, 4, names)
12751275+ "$5" <> query -> parameter_loop(splitters, query, context, 5, names)
12761276+ "$6" <> query -> parameter_loop(splitters, query, context, 6, names)
12771277+ "$7" <> query -> parameter_loop(splitters, query, context, 7, names)
12781278+ "$8" <> query -> parameter_loop(splitters, query, context, 8, names)
12791279+ "$9" <> query -> parameter_loop(splitters, query, context, 9, names)
12801280+12811281+ // EQUALITY OPERATOR
12821282+ // If we see a check for equality we need to update the current state
12831283+ // accordingly.
12841284+ "=" <> query ->
12851285+ case context {
12861286+ NoIdentifier | QueryParameterEquality(_) | IdentifierEquality(_) ->
12871287+ recover_from_error(splitters, query, names)
12881288+ QueryParameter(parameter) ->
12891289+ parameter_names_loop(
12901290+ splitters,
12911291+ query,
12921292+ QueryParameterEquality(parameter),
12931293+ names,
12941294+ )
12951295+ Identifier(identifier) ->
12961296+ parameter_names_loop(
12971297+ splitters,
12981298+ query,
12991299+ IdentifierEquality(identifier),
13001300+ names,
13011301+ )
13021302+ }
13031303+13041304+ // EMPTY STRING
13051305+ // That means we're done, there's nothing left to do and we can return all
13061306+ // the names we've figured out.
13071307+ "" -> names
13081308+13091309+ // UNEXPECTED CHARACTER
13101310+ // If we see anything strange that doesn't really look like an identifier
13111311+ // or a keyword we will just ignore them and jump forward to the end of the
13121312+ // word/operator/number/unexpected character.
13131313+ _ -> recover_from_error(splitters, query, names)
13141314+ }
13151315+}
13161316+13171317+fn parameter_loop(
13181318+ splitters: Splitters,
13191319+ query: String,
13201320+ context: Context,
13211321+ number: Int,
13221322+ names: Dict(Int, List(Name)),
13231323+) -> Dict(Int, List(Name)) {
13241324+ case query {
13251325+ "0" <> query ->
13261326+ parameter_loop(splitters, query, context, number * 10, names)
13271327+ "1" <> query ->
13281328+ parameter_loop(splitters, query, context, number * 10 + 1, names)
13291329+ "2" <> query ->
13301330+ parameter_loop(splitters, query, context, number * 10 + 2, names)
13311331+ "3" <> query ->
13321332+ parameter_loop(splitters, query, context, number * 10 + 3, names)
13331333+ "4" <> query ->
13341334+ parameter_loop(splitters, query, context, number * 10 + 4, names)
13351335+ "5" <> query ->
13361336+ parameter_loop(splitters, query, context, number * 10 + 5, names)
13371337+ "6" <> query ->
13381338+ parameter_loop(splitters, query, context, number * 10 + 6, names)
13391339+ "7" <> query ->
13401340+ parameter_loop(splitters, query, context, number * 10 + 7, names)
13411341+ "8" <> query ->
13421342+ parameter_loop(splitters, query, context, number * 10 + 8, names)
13431343+ "9" <> query ->
13441344+ parameter_loop(splitters, query, context, number * 10 + 9, names)
13451345+13461346+ // We've found a whitespace, so we know the query parameter is over with, we
13471347+ // can now check the context and see if we can record any name for it.
13481348+ "" | " " <> _ | "\n" <> _ | "\r" <> _ | "\t" <> _ ->
13491349+ add_parameter(splitters, query, number, context, names)
13501350+13511351+ // We've found a strange character meaning that this is not actually a query
13521352+ // parameter, but most likely a syntax error. We just bail out and go back
13531353+ // to the main loop with a new empty context to avoid this error propagating
13541354+ // further and having us infer invalid names.
13551355+ _ -> recover_from_error(splitters, query, names)
13561356+ }
13571357+}
13581358+13591359+fn string_loop(
13601360+ splitters: Splitters,
13611361+ query: String,
13621362+ names: Dict(Int, List(Name)),
13631363+) -> Dict(Int, List(Name)) {
13641364+ case splitter.split(splitters.strings, query) {
13651365+ // The string is over
13661366+ #(_, "'", query) ->
13671367+ parameter_names_loop(splitters, query, NoIdentifier, names)
13681368+13691369+ // We've found the escaped closing character, that means we're still inside
13701370+ // the string and have to keep going.
13711371+ #(_, "''", query) | #(_, "\\'", query) ->
13721372+ string_loop(splitters, query, names)
13731373+13741374+ // This is a string that is not closed, we just return the names we've
13751375+ // figured out so far
13761376+ #(_, _, _) -> names
13771377+ }
13781378+}
13791379+13801380+fn multiline_comment_loop(
13811381+ splitters: Splitters,
13821382+ query: String,
13831383+ context: Context,
13841384+ expected_closing_sequences: Int,
13851385+ names: Dict(Int, List(Name)),
13861386+) -> Dict(Int, List(Name)) {
13871387+ case expected_closing_sequences {
13881388+ 0 -> parameter_names_loop(splitters, query, context, names)
13891389+ _ ->
13901390+ case splitter.split(splitters.comments, query) {
13911391+ #(_, "*/", query) ->
13921392+ multiline_comment_loop(
13931393+ splitters,
13941394+ query,
13951395+ context,
13961396+ expected_closing_sequences - 1,
13971397+ names,
13981398+ )
13991399+ #(_, "/*", query) ->
14001400+ multiline_comment_loop(
14011401+ splitters,
14021402+ query,
14031403+ context,
14041404+ expected_closing_sequences + 1,
14051405+ names,
14061406+ )
14071407+ // This means the comments are unbalanced, so the query is not right.
14081408+ // We just return the names we've found so far!
14091409+ _ -> names
14101410+ }
14111411+ }
14121412+}
14131413+14141414+fn identifier_loop(
14151415+ splitters: Splitters,
14161416+ query: String,
14171417+ identifier: String,
14181418+ context: Context,
14191419+ names: Dict(Int, List(Name)),
14201420+) -> Dict(Int, List(Name)) {
14211421+ case query {
14221422+ "a" as letter <> query
14231423+ | "b" as letter <> query
14241424+ | "c" as letter <> query
14251425+ | "d" as letter <> query
14261426+ | "e" as letter <> query
14271427+ | "f" as letter <> query
14281428+ | "g" as letter <> query
14291429+ | "h" as letter <> query
14301430+ | "i" as letter <> query
14311431+ | "j" as letter <> query
14321432+ | "k" as letter <> query
14331433+ | "l" as letter <> query
14341434+ | "m" as letter <> query
14351435+ | "n" as letter <> query
14361436+ | "o" as letter <> query
14371437+ | "p" as letter <> query
14381438+ | "q" as letter <> query
14391439+ | "r" as letter <> query
14401440+ | "s" as letter <> query
14411441+ | "t" as letter <> query
14421442+ | "u" as letter <> query
14431443+ | "v" as letter <> query
14441444+ | "w" as letter <> query
14451445+ | "x" as letter <> query
14461446+ | "y" as letter <> query
14471447+ | "z" as letter <> query
14481448+ | "0" as letter <> query
14491449+ | "1" as letter <> query
14501450+ | "2" as letter <> query
14511451+ | "3" as letter <> query
14521452+ | "4" as letter <> query
14531453+ | "5" as letter <> query
14541454+ | "6" as letter <> query
14551455+ | "7" as letter <> query
14561456+ | "8" as letter <> query
14571457+ | "9" as letter <> query
14581458+ | "_" as letter <> query ->
14591459+ identifier_loop(splitters, query, identifier <> letter, context, names)
14601460+14611461+ "." <> query ->
14621462+ dotted_identifier(splitters, query, context, identifier, names)
14631463+14641464+ // The identifier is over, we can update the context accordingly!
14651465+ _ -> add_identifier(splitters, query, Column(identifier), context, names)
14661466+ }
14671467+}
14681468+14691469+fn dotted_identifier(
14701470+ splitters: Splitters,
14711471+ query: String,
14721472+ context: Context,
14731473+ table: String,
14741474+ names: Dict(Int, List(Name)),
14751475+) -> Dict(Int, List(Name)) {
14761476+ case query {
14771477+ // This is a quoted dotted identifier: `user."table"`
14781478+ "\"" <> query ->
14791479+ case string.split_once(query, "\"") {
14801480+ // The identifier is not closed, this is a syntax error, we just return
14811481+ // all the names we've figured out so far
14821482+ Error(_) -> names
14831483+ // We've found an identifier, and so update the names and context
14841484+ // accordingly.
14851485+ Ok(#(column, query)) -> {
14861486+ let name = TableAndColumn(table, column)
14871487+ add_identifier(splitters, query, name, context, names)
14881488+ }
14891489+ }
14901490+14911491+ // Otherwise we treat it as a regular dotted identifier.
14921492+ _ -> dotted_identifier_loop(splitters, query, context, table, "", names)
14931493+ }
14941494+}
14951495+14961496+fn dotted_identifier_loop(
14971497+ splitters: Splitters,
14981498+ query: String,
14991499+ context: Context,
15001500+ table: String,
15011501+ column: String,
15021502+ names: Dict(Int, List(Name)),
15031503+) -> Dict(Int, List(Name)) {
15041504+ case query {
15051505+ "a" as letter <> query
15061506+ | "b" as letter <> query
15071507+ | "c" as letter <> query
15081508+ | "d" as letter <> query
15091509+ | "e" as letter <> query
15101510+ | "f" as letter <> query
15111511+ | "g" as letter <> query
15121512+ | "h" as letter <> query
15131513+ | "i" as letter <> query
15141514+ | "j" as letter <> query
15151515+ | "k" as letter <> query
15161516+ | "l" as letter <> query
15171517+ | "m" as letter <> query
15181518+ | "n" as letter <> query
15191519+ | "o" as letter <> query
15201520+ | "p" as letter <> query
15211521+ | "q" as letter <> query
15221522+ | "r" as letter <> query
15231523+ | "s" as letter <> query
15241524+ | "t" as letter <> query
15251525+ | "u" as letter <> query
15261526+ | "v" as letter <> query
15271527+ | "w" as letter <> query
15281528+ | "x" as letter <> query
15291529+ | "y" as letter <> query
15301530+ | "z" as letter <> query
15311531+ | "0" as letter <> query
15321532+ | "1" as letter <> query
15331533+ | "2" as letter <> query
15341534+ | "3" as letter <> query
15351535+ | "4" as letter <> query
15361536+ | "5" as letter <> query
15371537+ | "6" as letter <> query
15381538+ | "7" as letter <> query
15391539+ | "8" as letter <> query
15401540+ | "9" as letter <> query
15411541+ | "_" as letter <> query -> {
15421542+ let column = column <> letter
15431543+ dotted_identifier_loop(splitters, query, context, table, column, names)
15441544+ }
1080154510811081-fn prepend_if(list: List(a), condition: Bool, item: a) -> List(a) {
10821082- case condition {
10831083- True -> [item, ..list]
10841084- False -> list
15461546+ // The identifier is over, we can update the context accordingly!
15471547+ _ -> {
15481548+ let name = TableAndColumn(table, column)
15491549+ add_identifier(splitters, query, name, context, names)
15501550+ }
10851551 }
10861552}
15531553+15541554+fn add_identifier(
15551555+ splitters: Splitters,
15561556+ query: String,
15571557+ identifier: Name,
15581558+ context: Context,
15591559+ names: Dict(Int, List(Name)),
15601560+) -> Dict(Int, List(Name)) {
15611561+ case context {
15621562+ // We're not comparing the identifier agains a query parameter, so we keep
15631563+ // track of this new latest identifier we might be interested in!
15641564+ IdentifierEquality(_) | Identifier(_) | NoIdentifier | QueryParameter(_) ->
15651565+ parameter_names_loop(splitters, query, Identifier(identifier), names)
15661566+15671567+ // We've found `$1 = identifier`, that means we have to record the new
15681568+ // name and reset the context!
15691569+ QueryParameterEquality(number) -> {
15701570+ let names = add_name(names, number, identifier)
15711571+ parameter_names_loop(splitters, query, NoIdentifier, names)
15721572+ }
15731573+ }
15741574+}
15751575+15761576+fn add_parameter(
15771577+ splitters: Splitters,
15781578+ query: String,
15791579+ parameter: Int,
15801580+ context: Context,
15811581+ names: Dict(Int, List(Name)),
15821582+) -> Dict(Int, List(Name)) {
15831583+ case context {
15841584+ NoIdentifier | Identifier(_) | QueryParameter(_) ->
15851585+ parameter_names_loop(splitters, query, QueryParameter(parameter), names)
15861586+15871587+ // We've found something like `$1 = $2`
15881588+ QueryParameterEquality(_) ->
15891589+ parameter_names_loop(splitters, query, NoIdentifier, names)
15901590+15911591+ // We've found something like `wibble = $1`, we have to keep track of
15921592+ // this new name we've found for the parameter we've just parsed.
15931593+ IdentifierEquality(identifier) -> {
15941594+ let names = add_name(names, parameter, identifier)
15951595+ parameter_names_loop(splitters, query, NoIdentifier, names)
15961596+ }
15971597+ }
15981598+}
15991599+16001600+fn recover_from_error(
16011601+ splitters: Splitters,
16021602+ query: String,
16031603+ names: Dict(Int, List(Name)),
16041604+) -> Dict(Int, List(Name)) {
16051605+ let #(_, _, query) = splitter.split(splitters.whitespace, query)
16061606+ parameter_names_loop(splitters, query, NoIdentifier, names)
16071607+}
+158
test/squirrel_test.gleam
···877877 squirrel.compare_code_snippets(expected_code, actual_code)
878878}
879879880880+// --- PARAMETER NAME INFERENCE ------------------------------------------------
881881+// This is to make sure we can infer names correctly from queries, and that we
882882+// generate proper code for those.
883883+//
884884+885885+pub fn can_infer_simple_name_on_left_of_equality_test() {
886886+ should_codegen(
887887+ "
888888+with squirrel_user as (select 1 as id, 'Louis' as name)
889889+select name from squirrel_user where id = $1
890890+",
891891+ )
892892+ |> birdie.snap(title: "can infer simple name on left of equality")
893893+}
894894+895895+pub fn can_infer_simple_quoted_name_on_left_of_equality_test() {
896896+ should_codegen(
897897+ "
898898+with squirrel_user as (select 1 as \"squirrel id\", 'Louis' as name)
899899+select name from squirrel_user where \"squirrel id\" = $1
900900+",
901901+ )
902902+ |> birdie.snap(title: "can infer simple quoted name on left of equality")
903903+}
904904+905905+pub fn can_infer_simple_name_on_right_of_equality_test() {
906906+ should_codegen(
907907+ "
908908+with squirrel_user as (select 1 as id, 'Louis' as name)
909909+select name from squirrel_user where $1 = id
910910+",
911911+ )
912912+ |> birdie.snap(title: "can infer simple name on right of equality")
913913+}
914914+915915+pub fn can_infer_simple_quoted_name_on_right_of_equality_test() {
916916+ should_codegen(
917917+ "
918918+with squirrel_user as (select 1 as \"squirrel id\", 'Louis' as name)
919919+select name from squirrel_user where $1 = \"squirrel id\"
920920+",
921921+ )
922922+ |> birdie.snap(title: "can infer simple quoted name on right of equality")
923923+}
924924+925925+pub fn can_infer_table_access_on_left_of_equality_test() {
926926+ should_codegen(
927927+ "
928928+with squirrel_user as (select 1 as id, 'Louis' as name)
929929+select name from squirrel_user where squirrel_user.id = $1
930930+",
931931+ )
932932+ |> birdie.snap(title: "can infer table access on left of equality")
933933+}
934934+935935+pub fn can_infer_quoted_table_access_on_left_of_equality_test() {
936936+ should_codegen(
937937+ "
938938+with squirrel_user as (select 1 as \"special id\", 'Louis' as name)
939939+select name from squirrel_user where squirrel_user.\"special id\" = $1
940940+",
941941+ )
942942+ |> birdie.snap(title: "can infer quoted table access on left of equality")
943943+}
944944+945945+pub fn can_infer_table_access_on_right_of_equality_test() {
946946+ should_codegen(
947947+ "
948948+with squirrel_user as (select 1 as id, 'Louis' as name)
949949+select name from squirrel_user where $1 = squirrel_user.id
950950+",
951951+ )
952952+ |> birdie.snap(title: "can infer table access on right of equality")
953953+}
954954+955955+pub fn can_infer_quoted_table_access_on_right_of_equality_test() {
956956+ should_codegen(
957957+ "
958958+with squirrel_user as (select 1 as \"special id\", 'Louis' as name)
959959+select name from squirrel_user where $1 = squirrel_user.\"special id\"
960960+",
961961+ )
962962+ |> birdie.snap(title: "can infer quoted table access on right of equality")
963963+}
964964+965965+pub fn can_infer_multiple_arguments_test() {
966966+ should_codegen(
967967+ "
968968+with squirrel_user as (select 1 as squirrel_user_id, 'Louis' as name)
969969+select name
970970+from squirrel_user
971971+where $1 = squirrel_user_id
972972+and squirrel_user.name = $2
973973+",
974974+ )
975975+ |> birdie.snap(title: "can infer multiple arguments")
976976+}
977977+978978+pub fn comments_are_ignored_when_inferring_parameters_names_test() {
979979+ should_codegen(
980980+ "
981981+with squirrel_user as (select 1 as squirrel_user_id, 'Louis' as name)
982982+select name
983983+from squirrel_user
984984+-- $1 = id
985985+where $1 = name
986986+-- $1 = id
987987+",
988988+ )
989989+ |> birdie.snap(title: "comments are ignored when inferring parameters names")
990990+}
991991+992992+pub fn multiline_comments_are_ignored_when_inferring_parameters_names_test() {
993993+ should_codegen(
994994+ "
995995+with squirrel_user as (select 1 as squirrel_user_id, 'Louis' as name)
996996+select name
997997+from squirrel_user
998998+/* $1 = id */
999999+where $1 = name
10001000+/* $1 = id */
10011001+",
10021002+ )
10031003+ |> birdie.snap(
10041004+ title: "multiline comments are ignored when inferring parameters names",
10051005+ )
10061006+}
10071007+10081008+pub fn multiline_nested_comments_are_ignored_when_inferring_parameters_names_test() {
10091009+ should_codegen(
10101010+ "
10111011+with squirrel_user as (select 1 as squirrel_user_id, 'Louis' as name)
10121012+select name
10131013+from squirrel_user
10141014+/* $1 = id /* $1 = id */ */
10151015+where $1 = name
10161016+/* $1 = id /* $1 = id */ */
10171017+",
10181018+ )
10191019+ |> birdie.snap(
10201020+ title: "multiline nested comments are ignored when inferring parameters names",
10211021+ )
10221022+}
10231023+10241024+pub fn strings_are_ignored_when_inferring_parameters_names_test() {
10251025+ should_codegen(
10261026+ "
10271027+with squirrel_user as (select 1 as squirrel_user_id, 'Louis' as name)
10281028+select name
10291029+from squirrel_user
10301030+where $1 = '$1 = id'
10311031+and $1 = name
10321032+and $1 = '$1 = id'
10331033+",
10341034+ )
10351035+ |> birdie.snap(title: "strings are ignored when inferring parameters names")
10361036+}
10371037+8801038// --- REGRESSIONS -------------------------------------------------------------
8811039// Bugs reported from GitHub issues so I make sure those will no longer pop up.
8821040//