From d2837b7194969e834fffe4f13070796fb1462fda Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 7 Jul 2026 07:10:27 +0000 Subject: [PATCH 1/7] core: land xqlc catalog + analyzer on sqlc's AST Port xqlc's core catalog (SQLite-backed sql_* catalog) and its dialect-neutral query analyzer into internal/core, repointing the analyzer from xqlc's copy of the AST onto sqlc's internal/sql/ast so there is a single AST. No converter and no second AST package. A smoke test drives the analyzer with sqlc's own PostgreSQL parser to prove the repointed analyzer resolves columns, types, star expansion, and aliases end-to-end against internal/sql/ast. This is the first step of merging xqlc back into sqlc as the future analysis core; ClickHouse will be the first engine wired onto it. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XTGxNHW6v1S1YyC9FDSgrK --- go.mod | 11 +- go.sum | 49 +++- internal/core/analysis.go | 81 ++++++ internal/core/analyzer/analyzer.go | 152 ++++++++++ internal/core/analyzer/analyzer_test.go | 107 +++++++ internal/core/analyzer/expr.go | 364 ++++++++++++++++++++++++ internal/core/analyzer/projection.go | 117 ++++++++ internal/core/analyzer/scope.go | 162 +++++++++++ internal/core/attribute.go | 220 ++++++++++++++ internal/core/cast.go | 77 +++++ internal/core/cast_test.go | 53 ++++ internal/core/catalog.go | 79 +++++ internal/core/class.go | 42 +++ internal/core/constraint.go | 17 ++ internal/core/dialect.go | 49 ++++ internal/core/dialect_test.go | 44 +++ internal/core/namespace.go | 22 ++ internal/core/operator.go | 76 +++++ internal/core/operator_test.go | 48 ++++ internal/core/proc.go | 154 ++++++++++ internal/core/proc_test.go | 69 +++++ internal/core/schema.sql | 156 ++++++++++ internal/core/types.go | 137 +++++++++ 23 files changed, 2283 insertions(+), 3 deletions(-) create mode 100644 internal/core/analysis.go create mode 100644 internal/core/analyzer/analyzer.go create mode 100644 internal/core/analyzer/analyzer_test.go create mode 100644 internal/core/analyzer/expr.go create mode 100644 internal/core/analyzer/projection.go create mode 100644 internal/core/analyzer/scope.go create mode 100644 internal/core/attribute.go create mode 100644 internal/core/cast.go create mode 100644 internal/core/cast_test.go create mode 100644 internal/core/catalog.go create mode 100644 internal/core/class.go create mode 100644 internal/core/constraint.go create mode 100644 internal/core/dialect.go create mode 100644 internal/core/dialect_test.go create mode 100644 internal/core/namespace.go create mode 100644 internal/core/operator.go create mode 100644 internal/core/operator_test.go create mode 100644 internal/core/proc.go create mode 100644 internal/core/proc_test.go create mode 100644 internal/core/schema.sql create mode 100644 internal/core/types.go diff --git a/go.mod b/go.mod index c89cfa38f8..d8633c12c2 100644 --- a/go.mod +++ b/go.mod @@ -30,27 +30,36 @@ require ( google.golang.org/grpc v1.81.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.48.1 ) require ( cel.dev/expr v0.25.1 // indirect filippo.io/edwards25519 v1.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kr/text v0.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-sqlite3-wasm/v2 v2.6.35302 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/julianday v1.0.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + modernc.org/libc v1.70.0 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 017fc2647b..3298bf1540 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYY github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= @@ -27,8 +29,12 @@ github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -47,16 +53,22 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-sqlite3 v0.34.4 h1:bp8jd1o2CMvjkrrp1lOtHDu1TdzExNWACt+piQeMwzg= github.com/ncruces/go-sqlite3 v0.34.4/go.mod h1:tOyhDWnzlrzflKIGw177imjuO4ZEbfOS1NJ67B1BanQ= github.com/ncruces/go-sqlite3-wasm/v2 v2.6.35302 h1:IZCiInPIp6OhOc1skMDGOBMwMQuE1TK+QqVe35vd/ro= github.com/ncruces/go-sqlite3-wasm/v2 v2.6.35302/go.mod h1:ELHF6yqC51E0DiitfabHXl/aKKouCihugbhNT5a+yEY= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/pganalyze/pg_query_go/v6 v6.2.2 h1:O0L6zMC226R82RF3X5n0Ki6HjytDsoAzuzp4ATVAHNo= github.com/pganalyze/pg_query_go/v6 v6.2.2/go.mod h1:Cn6+j4870kJz3iYNsb0VsNG04vpSWgEvBwc590J4qD0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ= github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= @@ -104,16 +116,21 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= -golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= @@ -130,3 +147,31 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA= +modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/core/analysis.go b/internal/core/analysis.go new file mode 100644 index 0000000000..2cb61cbf53 --- /dev/null +++ b/internal/core/analysis.go @@ -0,0 +1,81 @@ +package core + +// Command identifies the kind of statement that produced a PrepareResult. +// Only the four DML statements that can have a prepare-able shape are +// emitted; DDL and TCL produce an empty result with Command == "". +type Command string + +const ( + CommandSelect Command = "SELECT" + CommandInsert Command = "INSERT" + CommandUpdate Command = "UPDATE" + CommandDelete Command = "DELETE" +) + +// PrepareResult describes the output of preparing a SQL statement. +type PrepareResult struct { + Command Command `json:"command,omitempty"` + Columns []Column `json:"columns"` + Parameters []Parameter `json:"parameters"` +} + +// ColumnSource identifies the table column a result column or bind +// parameter is sourced from. All fields are optional; the struct is +// emitted only when at least one is populated. +// +// Schema / Table / Column are the *origin* identifiers (pre-alias) — +// they correspond to sqlite's `sqlite3_column_origin_name` and mysql's +// `org_table` / `org_name`. TableAlias is the name the query used to +// refer to the table; it lets codegen distinguish `t1` from `t2` in +// `SELECT t1.x, t2.x FROM t t1 JOIN t t2 ...`. +type ColumnSource struct { + Schema string `json:"schema,omitempty"` + Table string `json:"table,omitempty"` + TableAlias string `json:"table_alias,omitempty"` + Column string `json:"column,omitempty"` +} + +// Column describes a single output column from a prepared statement. +// +// SourceClassOID and SourceAttributeOID are populated when the column +// is a direct reference to a table column (i.e. it appears in +// sql_attribute); they are zero for computed/derived expressions like +// aggregates or arithmetic. Source is the human-readable resolution of +// those OIDs and is populated under the same conditions. +// +// DeclType, TypeLength, TypeScale, IsPrimaryKey, IsUnique, and +// IsAutoIncrement come from the resolved source attribute and are zero +// for computed expressions. +type Column struct { + Name string `json:"name"` + DataType string `json:"data_type"` + TypeOID int64 `json:"type_oid,omitempty"` + NotNull bool `json:"not_null"` + SourceClassOID int64 `json:"source_class_oid,omitempty"` + SourceAttributeOID int64 `json:"source_attribute_oid,omitempty"` + Source *ColumnSource `json:"source,omitempty"` + DeclType string `json:"decl_type,omitempty"` + TypeLength int `json:"type_length,omitempty"` + TypeScale int `json:"type_scale,omitempty"` + IsPrimaryKey bool `json:"is_primary_key,omitempty"` + IsUnique bool `json:"is_unique,omitempty"` + IsAutoIncrement bool `json:"is_auto_increment,omitempty"` +} + +// Parameter describes a bind parameter in a prepared statement. +// +// Number is the 1-based position of the parameter as it appeared in the +// source ($1, $2, ...). Name is populated for named-parameter dialects +// or when a sqlc-style "-- name:" annotation gave the param a name; it +// is empty otherwise. DataType / TypeOID / NotNull come from the +// resolved usage site (an operator overload, function argument, etc.). +// Source identifies the column the parameter binds against (e.g. +// `users.age` for `WHERE age > $1`), when one can be inferred. +type Parameter struct { + Number int `json:"number"` + Name string `json:"name,omitempty"` + DataType string `json:"data_type,omitempty"` + TypeOID int64 `json:"type_oid,omitempty"` + NotNull bool `json:"not_null"` + Source *ColumnSource `json:"source,omitempty"` +} diff --git a/internal/core/analyzer/analyzer.go b/internal/core/analyzer/analyzer.go new file mode 100644 index 0000000000..39da95585a --- /dev/null +++ b/internal/core/analyzer/analyzer.go @@ -0,0 +1,152 @@ +// Package analyzer implements a dialect-neutral SQL query analyzer that +// resolves names, types, operators, and parameters by querying the +// catalog (core.Catalog). It produces a core.PrepareResult. +// +// Scope is intentionally narrow in this iteration: single-relation SELECT +// queries, simple WHERE / GROUP BY / projection. JOINs, subqueries, +// CTEs, set ops, and DML RETURNING are intended follow-ups. +package analyzer + +import ( + "fmt" + + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// Prepare walks a parsed statement and produces a PrepareResult by +// querying the catalog for relations, types, operators, and casts. +// stmt can be a *ast.RawStmt (typical parser output) or an unwrapped +// statement node. +func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) { + if rs, ok := stmt.(*ast.RawStmt); ok { + stmt = rs.Stmt + } + a := &analyzer{ + cat: cat, + params: map[int]*core.Parameter{}, + } + switch s := stmt.(type) { + case *ast.SelectStmt: + if err := a.analyzeSelect(s); err != nil { + return core.PrepareResult{}, err + } + a.command = core.CommandSelect + default: + return core.PrepareResult{}, fmt.Errorf("analyzer: unsupported statement %T", stmt) + } + return a.result(), nil +} + +type analyzer struct { + cat *core.Catalog + scope *scope + columns []core.Column + params map[int]*core.Parameter + command core.Command +} + +func (a *analyzer) result() core.PrepareResult { + return core.PrepareResult{ + Command: a.command, + Columns: a.columns, + Parameters: orderedParams(a.params), + } +} + +func orderedParams(m map[int]*core.Parameter) []core.Parameter { + if len(m) == 0 { + return nil + } + maxN := 0 + for n := range m { + if n > maxN { + maxN = n + } + } + out := make([]core.Parameter, 0, len(m)) + for i := 1; i <= maxN; i++ { + if p, ok := m[i]; ok { + out = append(out, *p) + } + } + return out +} + +func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error { + sc, err := a.buildScope(s.FromClause) + if err != nil { + return err + } + a.scope = sc + + // Join ON conditions get typed against the (already-assembled) + // scope so they can reference columns from either side. + for _, item := range listItems(s.FromClause) { + if err := a.typeJoinConditions(item); err != nil { + return fmt.Errorf("join: %w", err) + } + } + + if s.WhereClause != nil { + if _, err := a.typeExpr(s.WhereClause); err != nil { + return fmt.Errorf("where: %w", err) + } + } + if items := listItems(s.GroupClause); items != nil { + for _, g := range items { + if _, err := a.typeExpr(g); err != nil { + return fmt.Errorf("group by: %w", err) + } + } + } + if s.HavingClause != nil { + if _, err := a.typeExpr(s.HavingClause); err != nil { + return fmt.Errorf("having: %w", err) + } + } + + targets := listItems(s.TargetList) + if targets == nil { + return fmt.Errorf("select: empty target list") + } + for _, t := range targets { + rt, ok := t.(*ast.ResTarget) + if !ok { + continue + } + if err := a.projectTarget(rt); err != nil { + return err + } + } + return nil +} + +func listItems(l *ast.List) []ast.Node { + if l == nil { + return nil + } + return l.Items +} + +// typeJoinConditions walks a FROM-list item and types every join's ON +// expression. USING clauses are skipped — the columns they reference +// already exist in scope, no expression to type. +func (a *analyzer) typeJoinConditions(item ast.Node) error { + je, ok := item.(*ast.JoinExpr) + if !ok { + return nil + } + if err := a.typeJoinConditions(je.Larg); err != nil { + return err + } + if err := a.typeJoinConditions(je.Rarg); err != nil { + return err + } + if je.Quals != nil { + if _, err := a.typeExpr(je.Quals); err != nil { + return fmt.Errorf("ON: %w", err) + } + } + return nil +} diff --git a/internal/core/analyzer/analyzer_test.go b/internal/core/analyzer/analyzer_test.go new file mode 100644 index 0000000000..b7300921b7 --- /dev/null +++ b/internal/core/analyzer/analyzer_test.go @@ -0,0 +1,107 @@ +package analyzer_test + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/core/analyzer" + "github.com/sqlc-dev/sqlc/internal/engine/postgresql" +) + +// seedUsers builds a minimal catalog with a single "users" table so the +// analyzer has something to resolve against. It deliberately avoids any +// per-dialect seed: the point is to exercise the dialect-neutral +// analyzer directly on sqlc's ast. +func seedUsers(t *testing.T) *core.Catalog { + t.Helper() + cat, err := core.New() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { cat.Close() }) + + ns, err := cat.NamespaceOID("public") + if err != nil { + t.Fatal(err) + } + int4, err := cat.CreateType("int4", 4) + if err != nil { + t.Fatal(err) + } + text, err := cat.CreateType("text", -1) + if err != nil { + t.Fatal(err) + } + users, err := cat.CreateClass(ns, "users", "r") + if err != nil { + t.Fatal(err) + } + if err := cat.CreateAttribute(users, "id", int4, true, false, 1); err != nil { + t.Fatal(err) + } + if err := cat.CreateAttribute(users, "name", text, true, false, 2); err != nil { + t.Fatal(err) + } + return cat +} + +// prepare parses query with sqlc's own PostgreSQL parser — which emits +// exactly the internal/sql/ast the analyzer was repointed onto — and +// runs the analyzer against cat. +func prepare(t *testing.T, cat *core.Catalog, query string) core.PrepareResult { + t.Helper() + stmts, err := postgresql.NewParser().Parse(strings.NewReader(query)) + if err != nil { + t.Fatalf("parse: %v", err) + } + if len(stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(stmts)) + } + res, err := analyzer.Prepare(cat, stmts[0].Raw) + if err != nil { + t.Fatalf("analyze: %v", err) + } + return res +} + +func TestPrepareSelectColumns(t *testing.T) { + cat := seedUsers(t) + res := prepare(t, cat, "SELECT id, name FROM users") + + if len(res.Columns) != 2 { + t.Fatalf("got %d cols, want 2: %+v", len(res.Columns), res.Columns) + } + if res.Columns[0].Name != "id" || res.Columns[0].DataType != "int4" || !res.Columns[0].NotNull { + t.Errorf("col 0: %+v", res.Columns[0]) + } + if res.Columns[1].Name != "name" || res.Columns[1].DataType != "text" || !res.Columns[1].NotNull { + t.Errorf("col 1: %+v", res.Columns[1]) + } + for i, c := range res.Columns { + if c.SourceClassOID == 0 || c.SourceAttributeOID == 0 { + t.Errorf("col %d %s missing source binding: %+v", i, c.Name, c) + } + } +} + +func TestPrepareSelectStar(t *testing.T) { + cat := seedUsers(t) + res := prepare(t, cat, "SELECT * FROM users") + + if len(res.Columns) != 2 { + t.Fatalf("got %d cols, want 2: %+v", len(res.Columns), res.Columns) + } + if res.Columns[0].Name != "id" || res.Columns[1].Name != "name" { + t.Errorf("got %q, %q; want id, name", res.Columns[0].Name, res.Columns[1].Name) + } +} + +func TestPrepareAlias(t *testing.T) { + cat := seedUsers(t) + res := prepare(t, cat, "SELECT id AS user_id FROM users u") + + if len(res.Columns) != 1 || res.Columns[0].Name != "user_id" { + t.Fatalf("got %+v", res.Columns) + } +} diff --git a/internal/core/analyzer/expr.go b/internal/core/analyzer/expr.go new file mode 100644 index 0000000000..fdf9739554 --- /dev/null +++ b/internal/core/analyzer/expr.go @@ -0,0 +1,364 @@ +package analyzer + +import ( + "fmt" + "strings" + + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// exprType is the analyzer's resolved type for an expression: the +// catalog type OID, nullability, and (when present) the source +// attribute for direct column refs. +type exprType struct { + typeOID int64 + nullable bool + sourceClassOID int64 + sourceAttributeOID int64 + // sourceTableAlias is the user-visible name of the relation the + // source attribute came from — i.e. the FROM-list alias, or the + // table name when no alias was given. Empty for computed + // expressions. + sourceTableAlias string +} + +// typeExpr recursively types an expression, side-effecting parameter +// inference along the way. Returns the expression's exprType. +func (a *analyzer) typeExpr(n ast.Node) (exprType, error) { + switch e := n.(type) { + case nil: + return exprType{}, nil + + case *ast.TODO: + // Parser emits TODO for fields it didn't fully translate (e.g. an + // absent WHERE clause shows up as TODO rather than nil). Treat as no-op. + return exprType{}, nil + + case *ast.A_Const: + return a.typeConst(e) + + case *ast.ColumnRef: + return a.typeColumnRef(e) + + case *ast.ParamRef: + return a.typeParamRef(e) + + case *ast.A_Expr: + return a.typeAExpr(e) + + case *ast.BoolExpr: + return a.typeBoolExpr(e) + + case *ast.FuncCall: + return a.typeFuncCall(e) + + case *ast.TypeCast: + return a.typeTypeCast(e) + + case *ast.NullTest: + // IS [NOT] NULL always returns boolean, regardless of operand type. + if _, err := a.typeExpr(e.Arg); err != nil { + return exprType{}, err + } + return a.boolType(false /* not nullable */) + } + return exprType{}, fmt.Errorf("typeExpr: unsupported %T", n) +} + +func (a *analyzer) typeConst(c *ast.A_Const) (exprType, error) { + switch v := c.Val.(type) { + case *ast.Integer: + _ = v + oid, err := a.cat.TypeOID("int4") + if err != nil { + return exprType{}, err + } + return exprType{typeOID: oid}, nil + case *ast.Float: + oid, err := a.cat.TypeOID("numeric") + if err != nil { + return exprType{}, err + } + return exprType{typeOID: oid}, nil + case *ast.String: + // PG bare string literals start as `unknown` but coerce contextually; + // for simplicity we type them as text and rely on casts later. + oid, err := a.cat.TypeOID("text") + if err != nil { + return exprType{}, err + } + return exprType{typeOID: oid}, nil + case *ast.Boolean: + return a.boolType(false) + case nil: + // NULL literal + return exprType{nullable: true}, nil + } + return exprType{}, fmt.Errorf("typeConst: unsupported %T", c.Val) +} + +func (a *analyzer) boolType(nullable bool) (exprType, error) { + oid, err := a.cat.TypeOID("bool") + if err != nil { + return exprType{}, err + } + return exprType{typeOID: oid, nullable: nullable}, nil +} + +func (a *analyzer) typeColumnRef(c *ast.ColumnRef) (exprType, error) { + parts := flattenFields(c.Fields) + if len(parts) == 0 { + return exprType{}, fmt.Errorf("column ref: empty") + } + relation := "" + column := parts[0] + if len(parts) >= 2 { + relation = parts[0] + column = parts[1] + } + rel, col, ok, err := a.scope.resolveColumn(relation, column) + if err != nil { + return exprType{}, err + } + if !ok { + if relation != "" { + return exprType{}, fmt.Errorf("unknown column %q.%q", relation, column) + } + return exprType{}, fmt.Errorf("unknown column %q", column) + } + return exprType{ + typeOID: col.typeOID, + nullable: !col.notNull, + sourceClassOID: rel.classOID, + sourceAttributeOID: col.attOID, + sourceTableAlias: rel.alias, + }, nil +} + +// flattenFields converts a ColumnRef.Fields list into a slice of strings, +// stopping at any *A_Star. +func flattenFields(fields *ast.List) []string { + if fields == nil { + return nil + } + out := make([]string, 0, len(fields.Items)) + for _, item := range fields.Items { + switch v := item.(type) { + case *ast.String: + out = append(out, v.Str) + case *ast.A_Star: + out = append(out, "*") + return out + } + } + return out +} + +func (a *analyzer) typeParamRef(p *ast.ParamRef) (exprType, error) { + cur, ok := a.params[p.Number] + if !ok { + cur = &core.Parameter{Number: p.Number} + a.params[p.Number] = cur + } + return exprType{typeOID: cur.TypeOID, nullable: !cur.NotNull}, nil +} + +// inferParam updates a previously-seen parameter's type from its +// usage context (typically the other side of a binary operator). +// Only fills in when not already set. +func (a *analyzer) inferParam(number int, t exprType) { + cur, ok := a.params[number] + if !ok { + cur = &core.Parameter{Number: number} + a.params[number] = cur + } + if cur.TypeOID == 0 && t.typeOID != 0 { + cur.TypeOID = t.typeOID + if name, err := a.cat.TypeName(t.typeOID); err == nil { + cur.DataType = name + } + cur.NotNull = !t.nullable + } + // Record the source column the parameter binds against (e.g. + // `WHERE age > $1` → users.age). Only set when not yet known so + // the first usage wins; subsequent appearances of the same param + // against a different column don't clobber it. + if cur.Source == nil && t.sourceAttributeOID != 0 { + ad, err := a.cat.LookupAttribute(t.sourceAttributeOID) + if err == nil { + cur.Source = &core.ColumnSource{ + Schema: ad.Schema, + Table: ad.Table, + TableAlias: t.sourceTableAlias, + Column: ad.Column, + } + } + } +} + +// typeAExpr handles binary and unary expressions. For now we only +// implement OP_OP (the standard binary/unary operator case); other +// kinds error out for visibility. +func (a *analyzer) typeAExpr(e *ast.A_Expr) (exprType, error) { + if e.Kind != ast.A_Expr_Kind_OP { + return exprType{}, fmt.Errorf("a_expr: unsupported kind %v", e.Kind) + } + opName := opNameFromList(e.Name) + if opName == "" { + return exprType{}, fmt.Errorf("a_expr: unnamed operator") + } + + leftT, err := a.typeExpr(e.Lexpr) + if err != nil { + return exprType{}, err + } + rightT, err := a.typeExpr(e.Rexpr) + if err != nil { + return exprType{}, err + } + + // Cross-infer parameter types from the non-param side. + if pr, ok := e.Lexpr.(*ast.ParamRef); ok && rightT.typeOID != 0 { + a.inferParam(pr.Number, rightT) + leftT = rightT + } + if pr, ok := e.Rexpr.(*ast.ParamRef); ok && leftT.typeOID != 0 { + a.inferParam(pr.Number, leftT) + rightT = leftT + } + + overload, err := a.resolveOperator(opName, leftT.typeOID, rightT.typeOID) + if err != nil { + return exprType{}, err + } + return exprType{ + typeOID: overload.ResultTypeOID, + nullable: leftT.nullable || rightT.nullable, + }, nil +} + +func opNameFromList(l *ast.List) string { + if l == nil { + return "" + } + parts := make([]string, 0, len(l.Items)) + for _, item := range l.Items { + if s, ok := item.(*ast.String); ok { + parts = append(parts, s.Str) + } + } + return strings.Join(parts, ".") +} + +// resolveOperator finds an operator overload, attempting implicit casts +// on either side if no exact match exists. +func (a *analyzer) resolveOperator(name string, leftOID, rightOID int64) (core.OperatorOverload, error) { + candidates, err := a.cat.FindOperators(name, leftOID, rightOID) + if err != nil { + return core.OperatorOverload{}, err + } + if len(candidates) > 0 { + return candidates[0], nil + } + + // Try implicit-cast both sides: enumerate all overloads of this name + // and pick the first whose operand types are reachable via implicit + // casts from our actual operand types. + all, err := a.cat.FindOperators(name, 0, 0) + if err != nil { + return core.OperatorOverload{}, err + } + for _, ov := range all { + if leftOID != 0 && ov.LeftTypeOID != 0 && leftOID != ov.LeftTypeOID { + ok, _ := a.cat.CastAllowed(leftOID, ov.LeftTypeOID, "i") + if !ok { + continue + } + } + if rightOID != 0 && ov.RightTypeOID != 0 && rightOID != ov.RightTypeOID { + ok, _ := a.cat.CastAllowed(rightOID, ov.RightTypeOID, "i") + if !ok { + continue + } + } + // Reject candidates with mismatched arity + if (leftOID == 0) != (ov.LeftTypeOID == 0) { + continue + } + if (rightOID == 0) != (ov.RightTypeOID == 0) { + continue + } + return ov, nil + } + return core.OperatorOverload{}, fmt.Errorf("no operator %q for (%d, %d)", name, leftOID, rightOID) +} + +func (a *analyzer) typeBoolExpr(b *ast.BoolExpr) (exprType, error) { + for _, item := range listItems(b.Args) { + if _, err := a.typeExpr(item); err != nil { + return exprType{}, err + } + } + return a.boolType(false) +} + +func (a *analyzer) typeFuncCall(f *ast.FuncCall) (exprType, error) { + // Function name comes through as a List of *String parts (qualified name) + // in either f.Funcname or f.Func. + name := funcCallName(f) + if name == "" { + return exprType{}, fmt.Errorf("func call: missing name") + } + + // COUNT(*) always returns bigint and is non-null. + if f.AggStar && (name == "count" || name == "count.*") { + oid, err := a.cat.TypeOID("int8") + if err != nil { + return exprType{}, err + } + return exprType{typeOID: oid, nullable: false}, nil + } + + // Type the args (we don't yet resolve overloads against argument types + // — we just take the first overload by name and trust the catalog). + for _, arg := range listItems(f.Args) { + if _, err := a.typeExpr(arg); err != nil { + return exprType{}, err + } + } + + overloads, err := a.cat.FindProcs(name, nil) + if err != nil { + return exprType{}, err + } + if len(overloads) == 0 { + return exprType{}, fmt.Errorf("unknown function %q", name) + } + p := overloads[0] + return exprType{typeOID: p.ReturnTypeOID, nullable: p.ReturnNullable}, nil +} + +func funcCallName(f *ast.FuncCall) string { + if f.Funcname != nil { + return opNameFromList(f.Funcname) + } + if f.Func != nil { + return strings.ToLower(f.Func.Name) + } + return "" +} + +func (a *analyzer) typeTypeCast(c *ast.TypeCast) (exprType, error) { + if c.TypeName == nil { + return exprType{}, fmt.Errorf("cast: missing target type") + } + if _, err := a.typeExpr(c.Arg); err != nil { + return exprType{}, err + } + oid, err := a.cat.TypeOID(strings.ToLower(c.TypeName.Name)) + if err != nil { + return exprType{}, fmt.Errorf("cast target %q: %w", c.TypeName.Name, err) + } + return exprType{typeOID: oid}, nil +} diff --git a/internal/core/analyzer/projection.go b/internal/core/analyzer/projection.go new file mode 100644 index 0000000000..067858111e --- /dev/null +++ b/internal/core/analyzer/projection.go @@ -0,0 +1,117 @@ +package analyzer + +import ( + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// projectTarget evaluates one entry in the SELECT list and records an +// output Column. Star expansions emit one column per source column. +func (a *analyzer) projectTarget(rt *ast.ResTarget) error { + if cr, ok := rt.Val.(*ast.ColumnRef); ok { + // "* " or "t.*" expansion + if isStarRef(cr) { + a.emitStar(cr) + return nil + } + } + + t, err := a.typeExpr(rt.Val) + if err != nil { + return err + } + col := core.Column{ + Name: targetName(rt), + TypeOID: t.typeOID, + NotNull: !t.nullable, + SourceClassOID: t.sourceClassOID, + SourceAttributeOID: t.sourceAttributeOID, + } + if t.typeOID != 0 { + if name, err := a.cat.TypeName(t.typeOID); err == nil { + col.DataType = name + } + } + a.decorateSource(&col, t.sourceAttributeOID, t.sourceTableAlias) + a.columns = append(a.columns, col) + return nil +} + +// decorateSource fills in the resolved Source struct and per-column +// flag fields for a Column whose value came from a real table column. +// Computed expressions (zero attOID) are left untouched. +func (a *analyzer) decorateSource(col *core.Column, attOID int64, tableAlias string) { + if attOID == 0 { + return + } + ad, err := a.cat.LookupAttribute(attOID) + if err != nil { + return + } + col.Source = &core.ColumnSource{ + Schema: ad.Schema, + Table: ad.Table, + TableAlias: tableAlias, + Column: ad.Column, + } + col.DeclType = ad.DeclType + col.TypeLength = ad.TypeLength + col.TypeScale = ad.TypeScale + col.IsPrimaryKey = ad.IsPrimaryKey + col.IsUnique = ad.IsUnique + col.IsAutoIncrement = ad.AutoIncrement +} + +// targetName picks the user-visible name for a SELECT-list entry: the +// alias if one was given, the source column name for direct refs, or +// the conventional "?column?" placeholder for computed expressions. +func targetName(rt *ast.ResTarget) string { + if rt.Name != nil && *rt.Name != "" { + return *rt.Name + } + if cr, ok := rt.Val.(*ast.ColumnRef); ok { + parts := flattenFields(cr.Fields) + if len(parts) > 0 { + return parts[len(parts)-1] + } + } + if fc, ok := rt.Val.(*ast.FuncCall); ok { + if name := funcCallName(fc); name != "" { + return name + } + } + return "?column?" +} + +func isStarRef(c *ast.ColumnRef) bool { + parts := flattenFields(c.Fields) + return len(parts) > 0 && parts[len(parts)-1] == "*" +} + +// emitStar expands * (or t.*) into one Column per source attribute. +func (a *analyzer) emitStar(cr *ast.ColumnRef) { + parts := flattenFields(cr.Fields) + relName := "" + if len(parts) > 1 { + relName = parts[0] + } + for _, rel := range a.scope.rels { + if relName != "" && rel.alias != relName { + continue + } + for _, c := range rel.cols { + col := core.Column{ + Name: c.name, + TypeOID: c.typeOID, + NotNull: c.notNull, + SourceClassOID: rel.classOID, + SourceAttributeOID: c.attOID, + } + if name, err := a.cat.TypeName(c.typeOID); err == nil { + col.DataType = name + } + a.decorateSource(&col, c.attOID, rel.alias) + a.columns = append(a.columns, col) + } + } +} diff --git a/internal/core/analyzer/scope.go b/internal/core/analyzer/scope.go new file mode 100644 index 0000000000..3d688228a4 --- /dev/null +++ b/internal/core/analyzer/scope.go @@ -0,0 +1,162 @@ +package analyzer + +import ( + "fmt" + + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// scope is the set of relations available to expression resolution at a +// given point in a query. For now it holds at most one parent (FROM +// list) — subquery / CTE / lateral parents are a follow-up. +type scope struct { + rels []scopeRel +} + +// scopeRel binds a FROM-list relation (table or aliased table) to the +// columns it contributes. +type scopeRel struct { + alias string // user-visible name for resolution; defaults to relation name + classOID int64 + cols []scopeCol +} + +// scopeCol is a single column visible through a scopeRel. +type scopeCol struct { + name string + attOID int64 + typeOID int64 + notNull bool +} + +// buildScope walks a FROM clause (currently: RangeVar plus JoinExpr +// trees, which we flatten into the rels slice) and produces a flat +// scope. Subqueries and table-valued functions are still TODO. +func (a *analyzer) buildScope(from *ast.List) (*scope, error) { + sc := &scope{} + for _, item := range listItems(from) { + if err := a.appendFromItem(sc, item); err != nil { + return nil, err + } + } + return sc, nil +} + +func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error { + switch v := item.(type) { + case *ast.RangeVar: + rel, err := a.bindRangeVar(v) + if err != nil { + return err + } + sc.rels = append(sc.rels, rel) + return nil + case *ast.JoinExpr: + // Flatten both sides into the same scope. The join condition + // (Quals / USING) is type-checked against the assembled scope + // in analyzeSelect after buildScope returns; we don't enforce + // it here. Outer-join nullability is also a follow-up. + if err := a.appendFromItem(sc, v.Larg); err != nil { + return err + } + return a.appendFromItem(sc, v.Rarg) + default: + return fmt.Errorf("scope: unsupported FROM item %T", item) + } +} + +func (a *analyzer) bindRangeVar(rv *ast.RangeVar) (scopeRel, error) { + if rv.Relname == nil { + return scopeRel{}, fmt.Errorf("range var: missing relation name") + } + relName := *rv.Relname + schema := "" + if rv.Schemaname != nil { + schema = *rv.Schemaname + } + if schema == "" { + schema = "public" + } + nsOID, err := a.cat.NamespaceOID(schema) + if err != nil { + return scopeRel{}, fmt.Errorf("schema %q: %w", schema, err) + } + classOID, err := a.cat.ClassOID(nsOID, relName) + if err != nil { + return scopeRel{}, fmt.Errorf("relation %q.%q: %w", schema, relName, err) + } + rel := scopeRel{ + alias: relName, + classOID: classOID, + } + if rv.Alias != nil && rv.Alias.Aliasname != nil && *rv.Alias.Aliasname != "" { + rel.alias = *rv.Alias.Aliasname + } + + cols, err := a.classColumns(classOID) + if err != nil { + return scopeRel{}, err + } + rel.cols = cols + return rel, nil +} + +// classColumns reads the column layout of a relation directly from +// sql_attribute. We can't go through TableColumns because that resolves +// by name and doesn't return attribute OIDs. +func (a *analyzer) classColumns(classOID int64) ([]scopeCol, error) { + rows, err := a.cat.DB().Query( + `SELECT a.oid, a.name, a.type_oid, a.not_null + FROM sql_attribute a WHERE a.class_oid = ? ORDER BY a.num`, + classOID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var out []scopeCol + for rows.Next() { + var c scopeCol + var nn int + if err := rows.Scan(&c.attOID, &c.name, &c.typeOID, &nn); err != nil { + return nil, err + } + c.notNull = nn != 0 + out = append(out, c) + } + return out, rows.Err() +} + +// resolveColumn locates a (relation, column) pair in the scope. +// relation may be empty for an unqualified reference; in that case we +// search every relation and error on ambiguity. +func (s *scope) resolveColumn(relation, column string) (rel scopeRel, col scopeCol, ok bool, err error) { + var matches []struct { + rel scopeRel + col scopeCol + } + for _, r := range s.rels { + if relation != "" && r.alias != relation { + continue + } + for _, c := range r.cols { + if c.name == column { + matches = append(matches, struct { + rel scopeRel + col scopeCol + }{r, c}) + } + } + } + if len(matches) == 0 { + return scopeRel{}, scopeCol{}, false, nil + } + if len(matches) > 1 { + return scopeRel{}, scopeCol{}, false, fmt.Errorf("ambiguous column reference %q", column) + } + return matches[0].rel, matches[0].col, true, nil +} + +// silence unused-import warning if core not referenced here directly +var _ = core.Column{} diff --git a/internal/core/attribute.go b/internal/core/attribute.go new file mode 100644 index 0000000000..95e6cc7be4 --- /dev/null +++ b/internal/core/attribute.go @@ -0,0 +1,220 @@ +package core + +import ( + "database/sql" + "fmt" +) + +// AttributeSpec carries the data needed to register a column on a +// relation. It is the full-fidelity counterpart to CreateAttribute, +// which is kept as a thin wrapper for callers that don't need to set +// any of the "extra" metadata. +type AttributeSpec struct { + ClassOID int64 + Name string + TypeOID int64 + Num int // 1-based ordinal position + NotNull bool + HasDefault bool + DeclType string // original declared type, before normalization + TypeLength int // varchar(N), numeric(p,s).p, etc. + TypeScale int // numeric(p,s).s + AutoIncrement bool + IsPrimaryKey bool + IsUnique bool +} + +// CreateAttributeSpec inserts a column with the full set of attribute +// metadata. +func (c *Catalog) CreateAttributeSpec(s AttributeSpec) error { + _, err := c.db.Exec(` + INSERT INTO sql_attribute ( + class_oid, name, type_oid, not_null, has_default, num, + decl_type, type_length, type_scale, + auto_increment, is_primary_key, is_unique + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + s.ClassOID, s.Name, s.TypeOID, + boolToInt(s.NotNull), boolToInt(s.HasDefault), s.Num, + s.DeclType, s.TypeLength, s.TypeScale, + boolToInt(s.AutoIncrement), + boolToInt(s.IsPrimaryKey), + boolToInt(s.IsUnique), + ) + if err != nil { + return fmt.Errorf("create attribute %q on class %d: %w", s.Name, s.ClassOID, err) + } + return nil +} + +// CreateAttribute inserts a column for the given relation. Wraps +// CreateAttributeSpec for callers that only set the basics. +func (c *Catalog) CreateAttribute(classOID int64, name string, typeOID int64, notNull bool, hasDefault bool, num int) error { + return c.CreateAttributeSpec(AttributeSpec{ + ClassOID: classOID, + Name: name, + TypeOID: typeOID, + Num: num, + NotNull: notNull, + HasDefault: hasDefault, + }) +} + +// SetAttributePrimaryKey flips the is_primary_key (and not_null) flag +// on every named column of a relation. Used when a table-level PRIMARY +// KEY constraint is processed after the columns have been registered. +func (c *Catalog) SetAttributePrimaryKey(classOID int64, columns []string) error { + for _, name := range columns { + _, err := c.db.Exec( + `UPDATE sql_attribute SET is_primary_key = 1, not_null = 1 + WHERE class_oid = ? AND name = ?`, + classOID, name, + ) + if err != nil { + return fmt.Errorf("mark pk %s on class %d: %w", name, classOID, err) + } + } + return nil +} + +// SetAttributeUnique flips is_unique on every named column. Multi-column +// UNIQUE constraints don't make individual columns unique, so callers +// should pass length-1 column lists only. +func (c *Catalog) SetAttributeUnique(classOID int64, columns []string) error { + for _, name := range columns { + _, err := c.db.Exec( + `UPDATE sql_attribute SET is_unique = 1 WHERE class_oid = ? AND name = ?`, + classOID, name, + ) + if err != nil { + return fmt.Errorf("mark unique %s on class %d: %w", name, classOID, err) + } + } + return nil +} + +// ColumnInfo describes a resolved column. +type ColumnInfo struct { + Name string + TypeName string + TypeOID int64 + NotNull bool + DeclType string + TypeLength int + TypeScale int + AutoIncrement bool + IsPrimaryKey bool + IsUnique bool + AttributeOID int64 + ClassOID int64 +} + +// ResolveColumn looks up a column by table name and column name. +func (c *Catalog) ResolveColumn(table, column string) (*ColumnInfo, error) { + var info ColumnInfo + var ai, ipk, iu int + err := c.db.QueryRow(` + SELECT a.oid, a.class_oid, a.name, t.name, a.type_oid, a.not_null, + a.decl_type, a.type_length, a.type_scale, + a.auto_increment, a.is_primary_key, a.is_unique + FROM sql_attribute a + JOIN sql_class c ON c.oid = a.class_oid + JOIN sql_type t ON t.oid = a.type_oid + WHERE c.name = ? AND a.name = ? + `, table, column).Scan( + &info.AttributeOID, &info.ClassOID, &info.Name, &info.TypeName, &info.TypeOID, &info.NotNull, + &info.DeclType, &info.TypeLength, &info.TypeScale, + &ai, &ipk, &iu, + ) + if err != nil { + return nil, fmt.Errorf("resolve %s.%s: %w", table, column, err) + } + info.AutoIncrement = ai != 0 + info.IsPrimaryKey = ipk != 0 + info.IsUnique = iu != 0 + return &info, nil +} + +// TableColumns returns all columns for a given table name, ordered by position. +func (c *Catalog) TableColumns(table string) ([]ColumnInfo, error) { + rows, err := c.db.Query(` + SELECT a.oid, a.class_oid, a.name, t.name, a.type_oid, a.not_null, + a.decl_type, a.type_length, a.type_scale, + a.auto_increment, a.is_primary_key, a.is_unique + FROM sql_attribute a + JOIN sql_class c ON c.oid = a.class_oid + JOIN sql_type t ON t.oid = a.type_oid + WHERE c.name = ? + ORDER BY a.num + `, table) + if err != nil { + return nil, fmt.Errorf("table columns %q: %w", table, err) + } + defer rows.Close() + + var cols []ColumnInfo + for rows.Next() { + var ci ColumnInfo + var ai, ipk, iu int + if err := rows.Scan( + &ci.AttributeOID, &ci.ClassOID, &ci.Name, &ci.TypeName, &ci.TypeOID, &ci.NotNull, + &ci.DeclType, &ci.TypeLength, &ci.TypeScale, + &ai, &ipk, &iu, + ); err != nil { + return nil, err + } + ci.AutoIncrement = ai != 0 + ci.IsPrimaryKey = ipk != 0 + ci.IsUnique = iu != 0 + cols = append(cols, ci) + } + return cols, rows.Err() +} + +// AttributeDetails describes a column resolved by its OID. Populated +// from sql_attribute joined back to sql_class / sql_namespace, so the +// caller doesn't have to re-resolve names from raw OIDs. +type AttributeDetails struct { + Schema string + Table string + Column string + Num int + DeclType string + TypeLength int + TypeScale int + AutoIncrement bool + IsPrimaryKey bool + IsUnique bool + NotNull bool +} + +// LookupAttribute returns the full source-side metadata for a column, +// keyed by attribute OID. Used by analyzers to populate Column.Source +// (and the per-column flag fields) once they've resolved an +// expression to a sql_attribute row. +func (c *Catalog) LookupAttribute(attOID int64) (AttributeDetails, error) { + var ad AttributeDetails + var schema sql.NullString + var ai, ipk, iu, nn int + err := c.db.QueryRow(` + SELECT ns.name, cls.name, a.name, a.num, + a.decl_type, a.type_length, a.type_scale, + a.auto_increment, a.is_primary_key, a.is_unique, a.not_null + FROM sql_attribute a + JOIN sql_class cls ON cls.oid = a.class_oid + JOIN sql_namespace ns ON ns.oid = cls.namespace_oid + WHERE a.oid = ? + `, attOID).Scan( + &schema, &ad.Table, &ad.Column, &ad.Num, + &ad.DeclType, &ad.TypeLength, &ad.TypeScale, + &ai, &ipk, &iu, &nn, + ) + if err != nil { + return ad, fmt.Errorf("lookup attribute %d: %w", attOID, err) + } + ad.Schema = schema.String + ad.AutoIncrement = ai != 0 + ad.IsPrimaryKey = ipk != 0 + ad.IsUnique = iu != 0 + ad.NotNull = nn != 0 + return ad, nil +} diff --git a/internal/core/cast.go b/internal/core/cast.go new file mode 100644 index 0000000000..96f02f839c --- /dev/null +++ b/internal/core/cast.go @@ -0,0 +1,77 @@ +package core + +import "fmt" + +// CastSpec describes a type-coercion rule. +// +// Context: +// - 'i' implicit — applied automatically by the analyzer +// - 'a' assignment — applied for INSERT/UPDATE assignments +// - 'e' explicit — only when the user writes CAST or :: +// +// ProcOID == 0 means binary-coercible (no function call required). +type CastSpec struct { + SourceTypeOID int64 + TargetTypeOID int64 + ProcOID int64 + Context string // 'i' | 'a' | 'e'; default 'e' + DialectOID int64 +} + +// CreateCast inserts a cast rule. (source, target) is the primary key, so +// re-inserting the same pair will fail; use ReplaceCast to overwrite. +func (c *Catalog) CreateCast(cs CastSpec) error { + if cs.Context == "" { + cs.Context = "e" + } + _, err := c.db.Exec( + `INSERT INTO sql_cast (source_type_oid, target_type_oid, proc_oid, context, dialect_oid) + VALUES (?, ?, ?, ?, ?)`, + cs.SourceTypeOID, cs.TargetTypeOID, + nullableOID(cs.ProcOID), cs.Context, nullableOID(cs.DialectOID), + ) + if err != nil { + return fmt.Errorf("create cast %d->%d: %w", cs.SourceTypeOID, cs.TargetTypeOID, err) + } + return nil +} + +// FindCast returns the cast rule from src to tgt, if one exists, plus +// whether it was found. +func (c *Catalog) FindCast(src, tgt int64) (CastSpec, bool, error) { + var cs CastSpec + var procOID, dialectOID int64 + err := c.db.QueryRow( + `SELECT source_type_oid, target_type_oid, + COALESCE(proc_oid, 0), context, COALESCE(dialect_oid, 0) + FROM sql_cast WHERE source_type_oid = ? AND target_type_oid = ?`, + src, tgt, + ).Scan(&cs.SourceTypeOID, &cs.TargetTypeOID, &procOID, &cs.Context, &dialectOID) + if err != nil { + return cs, false, nil + } + cs.ProcOID = procOID + cs.DialectOID = dialectOID + return cs, true, nil +} + +// CastAllowed reports whether src can be coerced to tgt in the given context. +// Same-type pairs always succeed. +func (c *Catalog) CastAllowed(src, tgt int64, ctx string) (bool, error) { + if src == tgt { + return true, nil + } + cs, ok, err := c.FindCast(src, tgt) + if err != nil || !ok { + return false, err + } + switch ctx { + case "i": + return cs.Context == "i", nil + case "a": + return cs.Context == "i" || cs.Context == "a", nil + case "e": + return true, nil + } + return false, fmt.Errorf("unknown cast context %q", ctx) +} diff --git a/internal/core/cast_test.go b/internal/core/cast_test.go new file mode 100644 index 0000000000..08c68b91c1 --- /dev/null +++ b/internal/core/cast_test.go @@ -0,0 +1,53 @@ +package core + +import "testing" + +func TestCastAllowed(t *testing.T) { + cat, err := New() + if err != nil { + t.Fatal(err) + } + defer cat.Close() + + intOID, _ := cat.CreateType("integer", 4) + bigintOID, _ := cat.CreateType("bigint", 8) + textOID, _ := cat.CreateType("text", 0) + + // integer -> bigint is implicit + if err := cat.CreateCast(CastSpec{ + SourceTypeOID: intOID, TargetTypeOID: bigintOID, Context: "i", + }); err != nil { + t.Fatal(err) + } + // integer -> text is explicit only + if err := cat.CreateCast(CastSpec{ + SourceTypeOID: intOID, TargetTypeOID: textOID, Context: "e", + }); err != nil { + t.Fatal(err) + } + + cases := []struct { + src, tgt int64 + ctx string + want bool + }{ + {intOID, intOID, "i", true}, // identity always OK + {intOID, bigintOID, "i", true}, // declared implicit + {intOID, bigintOID, "a", true}, // implicit subsumes assignment + {intOID, bigintOID, "e", true}, // implicit subsumes explicit + {intOID, textOID, "i", false}, // explicit-only blocked from implicit + {intOID, textOID, "a", false}, // and from assignment + {intOID, textOID, "e", true}, // but works for explicit + {textOID, intOID, "e", false}, // no rule defined + } + for _, c := range cases { + got, err := cat.CastAllowed(c.src, c.tgt, c.ctx) + if err != nil { + t.Errorf("CastAllowed(%d,%d,%q): %v", c.src, c.tgt, c.ctx, err) + continue + } + if got != c.want { + t.Errorf("CastAllowed(%d,%d,%q): got %v, want %v", c.src, c.tgt, c.ctx, got, c.want) + } + } +} diff --git a/internal/core/catalog.go b/internal/core/catalog.go new file mode 100644 index 0000000000..ed3a015a72 --- /dev/null +++ b/internal/core/catalog.go @@ -0,0 +1,79 @@ +// Package core provides a SQL catalog backed by an in-memory SQLite database. +// +// The catalog stores schema metadata (namespaces, types, tables, columns, +// constraints) in tables prefixed with sql_. Engine packages populate the +// catalog from DDL and then query it to resolve types during statement +// preparation. +package core + +import ( + "database/sql" + _ "embed" + "fmt" + + _ "modernc.org/sqlite" +) + +//go:embed schema.sql +var ddl string + +// Catalog is an in-memory SQLite database that stores schema metadata in +// sql_* tables modeled after PostgreSQL's system catalogs. +type Catalog struct { + db *sql.DB +} + +// Option configures a Catalog at creation time. The most common use is +// to seed dialect-specific built-ins; see WithSeed. +type Option func(*Catalog) error + +// WithSeed runs a seed function (typically dialect-provided) against the +// freshly bootstrapped catalog. Failures abort Catalog creation. +func WithSeed(fn func(*Catalog) error) Option { + return func(c *Catalog) error { return fn(c) } +} + +// New creates a new in-memory catalog and initializes the sql_* tables. +// Each Option runs after schema initialization and the default-namespace +// bootstrap; an Option that fails aborts catalog creation. +func New(opts ...Option) (*Catalog, error) { + db, err := sql.Open("sqlite", ":memory:") + if err != nil { + return nil, fmt.Errorf("core: open catalog: %w", err) + } + if _, err := db.Exec(ddl); err != nil { + db.Close() + return nil, fmt.Errorf("core: init schema: %w", err) + } + c := &Catalog{db: db} + if err := c.bootstrap(); err != nil { + db.Close() + return nil, fmt.Errorf("core: bootstrap: %w", err) + } + for i, opt := range opts { + if err := opt(c); err != nil { + db.Close() + return nil, fmt.Errorf("core: option %d: %w", i, err) + } + } + return c, nil +} + +// Close closes the underlying database connection. +func (c *Catalog) Close() error { + return c.db.Close() +} + +// DB returns the underlying *sql.DB for advanced queries. +func (c *Catalog) DB() *sql.DB { + return c.db +} + +// bootstrap seeds the catalog with the default "public" namespace. +// Built-in types and functions are added by per-dialect seed files, +// not here, so that nothing in the catalog is hardcoded to a single +// dialect's view of the world. +func (c *Catalog) bootstrap() error { + _, err := c.CreateNamespace("public") + return err +} diff --git a/internal/core/class.go b/internal/core/class.go new file mode 100644 index 0000000000..5aad1b21f0 --- /dev/null +++ b/internal/core/class.go @@ -0,0 +1,42 @@ +package core + +import "fmt" + +// CreateClass inserts a relation (table, view, etc.) and returns its OID. +// Kind should be 'r' (table), 'v' (view), or 'i' (index). +func (c *Catalog) CreateClass(namespaceOID int64, name string, kind string) (int64, error) { + res, err := c.db.Exec( + `INSERT INTO sql_class (namespace_oid, name, kind) VALUES (?, ?, ?)`, + namespaceOID, name, kind, + ) + if err != nil { + return 0, fmt.Errorf("create class %q: %w", name, err) + } + return res.LastInsertId() +} + +// ClassOID returns the OID for the given relation in the given namespace. +func (c *Catalog) ClassOID(namespaceOID int64, name string) (int64, error) { + var oid int64 + err := c.db.QueryRow( + `SELECT oid FROM sql_class WHERE namespace_oid = ? AND name = ?`, + namespaceOID, name, + ).Scan(&oid) + if err != nil { + return 0, fmt.Errorf("class %q: %w", name, err) + } + return oid, nil +} + +// ClassOIDByName looks up a relation by name across all namespaces. +// If multiple matches exist, it returns the first found. +func (c *Catalog) ClassOIDByName(name string) (int64, error) { + var oid int64 + err := c.db.QueryRow( + `SELECT oid FROM sql_class WHERE name = ? LIMIT 1`, name, + ).Scan(&oid) + if err != nil { + return 0, fmt.Errorf("class %q: %w", name, err) + } + return oid, nil +} diff --git a/internal/core/constraint.go b/internal/core/constraint.go new file mode 100644 index 0000000000..7f87479623 --- /dev/null +++ b/internal/core/constraint.go @@ -0,0 +1,17 @@ +package core + +import "fmt" + +// CreateConstraint inserts a constraint for the given relation. +// Kind should be 'p' (primary key), 'f' (foreign key), 'u' (unique), or 'c' (check). +// columns is a comma-separated list of attribute ordinal positions. +func (c *Catalog) CreateConstraint(classOID int64, name string, kind string, columns string) error { + _, err := c.db.Exec( + `INSERT INTO sql_constraint (class_oid, name, kind, columns) VALUES (?, ?, ?, ?)`, + classOID, name, kind, columns, + ) + if err != nil { + return fmt.Errorf("create constraint %q on class %d: %w", name, classOID, err) + } + return nil +} diff --git a/internal/core/dialect.go b/internal/core/dialect.go new file mode 100644 index 0000000000..283f63f57f --- /dev/null +++ b/internal/core/dialect.go @@ -0,0 +1,49 @@ +package core + +import "fmt" + +// CreateDialect inserts a SQL dialect and returns its OID. +func (c *Catalog) CreateDialect(name string) (int64, error) { + res, err := c.db.Exec(`INSERT INTO sql_dialect (name) VALUES (?)`, name) + if err != nil { + return 0, fmt.Errorf("create dialect %q: %w", name, err) + } + return res.LastInsertId() +} + +// DialectOID returns the OID for a registered dialect. +func (c *Catalog) DialectOID(name string) (int64, error) { + var oid int64 + err := c.db.QueryRow(`SELECT oid FROM sql_dialect WHERE name = ?`, name).Scan(&oid) + if err != nil { + return 0, fmt.Errorf("dialect %q: %w", name, err) + } + return oid, nil +} + +// SetDialectFlag stores a per-dialect configuration value. +// If the key already exists, the value is replaced. +func (c *Catalog) SetDialectFlag(dialectOID int64, key, value string) error { + _, err := c.db.Exec( + `INSERT INTO sql_dialect_flag (dialect_oid, key, value) VALUES (?, ?, ?) + ON CONFLICT(dialect_oid, key) DO UPDATE SET value = excluded.value`, + dialectOID, key, value, + ) + if err != nil { + return fmt.Errorf("set dialect flag %s.%s: %w", key, value, err) + } + return nil +} + +// DialectFlag returns the value of a dialect flag, or "" if not set. +func (c *Catalog) DialectFlag(dialectOID int64, key string) (string, error) { + var value string + err := c.db.QueryRow( + `SELECT value FROM sql_dialect_flag WHERE dialect_oid = ? AND key = ?`, + dialectOID, key, + ).Scan(&value) + if err != nil { + return "", nil // missing flag = empty string, not an error + } + return value, nil +} diff --git a/internal/core/dialect_test.go b/internal/core/dialect_test.go new file mode 100644 index 0000000000..e3818c3aab --- /dev/null +++ b/internal/core/dialect_test.go @@ -0,0 +1,44 @@ +package core + +import "testing" + +func TestDialectAndFlags(t *testing.T) { + cat, err := New() + if err != nil { + t.Fatal(err) + } + defer cat.Close() + + pgOID, err := cat.CreateDialect("postgresql") + if err != nil { + t.Fatal(err) + } + + got, err := cat.DialectOID("postgresql") + if err != nil || got != pgOID { + t.Fatalf("DialectOID: got %d (err=%v), want %d", got, err, pgOID) + } + + if err := cat.SetDialectFlag(pgOID, "identifier_case", "fold_lower"); err != nil { + t.Fatal(err) + } + v, err := cat.DialectFlag(pgOID, "identifier_case") + if err != nil || v != "fold_lower" { + t.Errorf("DialectFlag: got %q (err=%v), want fold_lower", v, err) + } + + // upsert + if err := cat.SetDialectFlag(pgOID, "identifier_case", "fold_upper"); err != nil { + t.Fatal(err) + } + v, _ = cat.DialectFlag(pgOID, "identifier_case") + if v != "fold_upper" { + t.Errorf("upsert: got %q, want fold_upper", v) + } + + // missing flag returns "" + v, err = cat.DialectFlag(pgOID, "nonexistent") + if err != nil || v != "" { + t.Errorf("missing flag: got %q (err=%v), want empty", v, err) + } +} diff --git a/internal/core/namespace.go b/internal/core/namespace.go new file mode 100644 index 0000000000..498f89d795 --- /dev/null +++ b/internal/core/namespace.go @@ -0,0 +1,22 @@ +package core + +import "fmt" + +// CreateNamespace inserts a namespace and returns its OID. +func (c *Catalog) CreateNamespace(name string) (int64, error) { + res, err := c.db.Exec(`INSERT INTO sql_namespace (name) VALUES (?)`, name) + if err != nil { + return 0, fmt.Errorf("create namespace %q: %w", name, err) + } + return res.LastInsertId() +} + +// NamespaceOID returns the OID for the given namespace name. +func (c *Catalog) NamespaceOID(name string) (int64, error) { + var oid int64 + err := c.db.QueryRow(`SELECT oid FROM sql_namespace WHERE name = ?`, name).Scan(&oid) + if err != nil { + return 0, fmt.Errorf("namespace %q: %w", name, err) + } + return oid, nil +} diff --git a/internal/core/operator.go b/internal/core/operator.go new file mode 100644 index 0000000000..2e77739901 --- /dev/null +++ b/internal/core/operator.go @@ -0,0 +1,76 @@ +package core + +import "fmt" + +// OperatorSpec describes an operator overload for insertion into the catalog. +// A NULL left_type_oid encodes a prefix unary operator; NULL right_type_oid +// encodes a postfix unary. +type OperatorSpec struct { + Name string + NamespaceOID int64 // 0 = NULL + DialectOID int64 // 0 = NULL (shared) + LeftTypeOID int64 // 0 = NULL + RightTypeOID int64 // 0 = NULL + ResultTypeOID int64 + ProcOID int64 // 0 = NULL +} + +// CreateOperator inserts an operator overload and returns its OID. +func (c *Catalog) CreateOperator(o OperatorSpec) (int64, error) { + res, err := c.db.Exec( + `INSERT INTO sql_operator + (namespace_oid, dialect_oid, name, + left_type_oid, right_type_oid, result_type_oid, proc_oid) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + nullableOID(o.NamespaceOID), nullableOID(o.DialectOID), o.Name, + nullableOID(o.LeftTypeOID), nullableOID(o.RightTypeOID), + o.ResultTypeOID, nullableOID(o.ProcOID), + ) + if err != nil { + return 0, fmt.Errorf("create operator %q: %w", o.Name, err) + } + return res.LastInsertId() +} + +// OperatorOverload is a resolved candidate from FindOperators. +type OperatorOverload struct { + OID int64 + Name string + LeftTypeOID int64 // 0 = NULL (prefix unary) + RightTypeOID int64 // 0 = NULL (postfix unary) + ResultTypeOID int64 + ProcOID int64 // 0 = NULL (binary-compatible) +} + +// FindOperators returns all operator overloads matching the given name and +// (left,right) operand types. A 0 type means "any" and skips the filter on +// that side; useful for listing all overloads of a name. +func (c *Catalog) FindOperators(name string, leftTypeOID, rightTypeOID int64) ([]OperatorOverload, error) { + q := `SELECT oid, name, + COALESCE(left_type_oid, 0), COALESCE(right_type_oid, 0), + result_type_oid, COALESCE(proc_oid, 0) + FROM sql_operator WHERE name = ?` + args := []any{name} + if leftTypeOID != 0 { + q += ` AND left_type_oid = ?` + args = append(args, leftTypeOID) + } + if rightTypeOID != 0 { + q += ` AND right_type_oid = ?` + args = append(args, rightTypeOID) + } + rows, err := c.db.Query(q, args...) + if err != nil { + return nil, fmt.Errorf("find operators %q: %w", name, err) + } + defer rows.Close() + var out []OperatorOverload + for rows.Next() { + var o OperatorOverload + if err := rows.Scan(&o.OID, &o.Name, &o.LeftTypeOID, &o.RightTypeOID, &o.ResultTypeOID, &o.ProcOID); err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} diff --git a/internal/core/operator_test.go b/internal/core/operator_test.go new file mode 100644 index 0000000000..c1631ce35f --- /dev/null +++ b/internal/core/operator_test.go @@ -0,0 +1,48 @@ +package core + +import "testing" + +func TestOperatorCreateAndFind(t *testing.T) { + cat, err := New() + if err != nil { + t.Fatal(err) + } + defer cat.Close() + + intOID, _ := cat.CreateType("integer", 4) + boolOID, _ := cat.CreateType("boolean", 1) + + // > (int, int) -> bool + gtOID, err := cat.CreateOperator(OperatorSpec{ + Name: ">", + LeftTypeOID: intOID, + RightTypeOID: intOID, + ResultTypeOID: boolOID, + }) + if err != nil { + t.Fatal(err) + } + if gtOID == 0 { + t.Fatal("expected non-zero operator oid") + } + + got, err := cat.FindOperators(">", intOID, intOID) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 { + t.Fatalf("FindOperators: want 1, got %d", len(got)) + } + if got[0].ResultTypeOID != boolOID { + t.Errorf("result type: got %d, want %d", got[0].ResultTypeOID, boolOID) + } + + // listing all overloads of > + all, err := cat.FindOperators(">", 0, 0) + if err != nil { + t.Fatal(err) + } + if len(all) != 1 { + t.Errorf("listing >: want 1, got %d", len(all)) + } +} diff --git a/internal/core/proc.go b/internal/core/proc.go new file mode 100644 index 0000000000..181b5d755f --- /dev/null +++ b/internal/core/proc.go @@ -0,0 +1,154 @@ +package core + +import ( + "database/sql" + "fmt" + "strings" +) + +// ProcSpec describes a function/aggregate/window/procedure for insertion +// into the catalog. +type ProcSpec struct { + Name string + NamespaceOID int64 // 0 = NULL + DialectOID int64 // 0 = NULL (shared) + Kind string // 'f'unc | 'a'gg | 'w'in | 'p'roc; default 'f' + ReturnTypeOID int64 + ReturnSet bool + ReturnNullable bool + Strict bool + VariadicKind string // 'n'one | 'a'rray | 'v'ariadic-any; default 'n' + Args []ProcArg +} + +// ProcArg is a single argument in a proc signature. +type ProcArg struct { + Name string + TypeOID int64 + Mode string // 'i'n | 'o'ut | 'b'oth | 't'able | 'v'ariadic; default 'i' + HasDefault bool +} + +// CreateProc inserts a proc and its arguments, returning the proc OID. +func (c *Catalog) CreateProc(p ProcSpec) (int64, error) { + if p.Kind == "" { + p.Kind = "f" + } + if p.VariadicKind == "" { + p.VariadicKind = "n" + } + res, err := c.db.Exec( + `INSERT INTO sql_proc + (namespace_oid, dialect_oid, name, kind, + return_type_oid, return_set, return_nullable, strict, variadic_kind) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + nullableOID(p.NamespaceOID), nullableOID(p.DialectOID), + strings.ToLower(p.Name), p.Kind, + p.ReturnTypeOID, boolToInt(p.ReturnSet), boolToInt(p.ReturnNullable), + boolToInt(p.Strict), p.VariadicKind, + ) + if err != nil { + return 0, fmt.Errorf("create proc %q: %w", p.Name, err) + } + procOID, err := res.LastInsertId() + if err != nil { + return 0, err + } + for i, a := range p.Args { + mode := a.Mode + if mode == "" { + mode = "i" + } + _, err := c.db.Exec( + `INSERT INTO sql_proc_arg (proc_oid, ord, name, type_oid, mode, has_default) + VALUES (?, ?, ?, ?, ?, ?)`, + procOID, i+1, a.Name, a.TypeOID, mode, boolToInt(a.HasDefault), + ) + if err != nil { + return 0, fmt.Errorf("create proc %q arg %d: %w", p.Name, i+1, err) + } + } + return procOID, nil +} + +// ProcOverload describes one resolved candidate from FindProcs. +type ProcOverload struct { + OID int64 + Name string + Kind string + ReturnTypeOID int64 + ReturnNullable bool + ArgTypes []int64 +} + +// FindProcs returns all procs matching the given (case-insensitive) name in +// any of the supplied namespaces. Pass an empty namespace list to search +// every namespace. Argument lists are populated in declaration order. +func (c *Catalog) FindProcs(name string, namespaceOIDs []int64) ([]ProcOverload, error) { + q := `SELECT oid, name, kind, return_type_oid, return_nullable + FROM sql_proc WHERE name = ?` + args := []any{strings.ToLower(name)} + if len(namespaceOIDs) > 0 { + q += " AND namespace_oid IN (" + placeholders(len(namespaceOIDs)) + ")" + for _, ns := range namespaceOIDs { + args = append(args, ns) + } + } + rows, err := c.db.Query(q, args...) + if err != nil { + return nil, fmt.Errorf("find procs %q: %w", name, err) + } + defer rows.Close() + var out []ProcOverload + for rows.Next() { + var o ProcOverload + var rn int + if err := rows.Scan(&o.OID, &o.Name, &o.Kind, &o.ReturnTypeOID, &rn); err != nil { + return nil, err + } + o.ReturnNullable = rn != 0 + out = append(out, o) + } + if err := rows.Err(); err != nil { + return nil, err + } + for i := range out { + argTypes, err := c.procArgTypes(out[i].OID) + if err != nil { + return nil, err + } + out[i].ArgTypes = argTypes + } + return out, nil +} + +func (c *Catalog) procArgTypes(procOID int64) ([]int64, error) { + rows, err := c.db.Query( + `SELECT type_oid FROM sql_proc_arg + WHERE proc_oid = ? AND mode IN ('i','b','v') + ORDER BY ord`, procOID, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var out []int64 + for rows.Next() { + var t int64 + if err := rows.Scan(&t); err != nil { + return nil, err + } + out = append(out, t) + } + return out, rows.Err() +} + +func placeholders(n int) string { + if n <= 0 { + return "" + } + return strings.Repeat("?,", n-1) + "?" +} + +// silence unused-import warning if we drop the sql.Null* usages +var _ sql.NullString diff --git a/internal/core/proc_test.go b/internal/core/proc_test.go new file mode 100644 index 0000000000..54ff899bcd --- /dev/null +++ b/internal/core/proc_test.go @@ -0,0 +1,69 @@ +package core + +import "testing" + +func TestProcCreateAndFind(t *testing.T) { + cat, err := New() + if err != nil { + t.Fatal(err) + } + defer cat.Close() + + intOID, err := cat.CreateType("integer", 4) + if err != nil { + t.Fatal(err) + } + textOID, err := cat.CreateType("text", 0) + if err != nil { + t.Fatal(err) + } + + // length(text) -> integer + procOID, err := cat.CreateProc(ProcSpec{ + Name: "length", + Kind: "f", + ReturnTypeOID: intOID, + ReturnNullable: true, + Args: []ProcArg{{Name: "s", TypeOID: textOID}}, + }) + if err != nil { + t.Fatalf("create length: %v", err) + } + if procOID == 0 { + t.Fatal("expected non-zero proc oid") + } + + // concat(text, text) -> text + if _, err := cat.CreateProc(ProcSpec{ + Name: "concat", + ReturnTypeOID: textOID, + Args: []ProcArg{ + {Name: "a", TypeOID: textOID}, + {Name: "b", TypeOID: textOID}, + }, + }); err != nil { + t.Fatal(err) + } + + got, err := cat.FindProcs("length", nil) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 { + t.Fatalf("FindProcs length: want 1, got %d", len(got)) + } + if got[0].ReturnTypeOID != intOID { + t.Errorf("length return type: got %d, want %d", got[0].ReturnTypeOID, intOID) + } + if len(got[0].ArgTypes) != 1 || got[0].ArgTypes[0] != textOID { + t.Errorf("length args: got %v, want [%d]", got[0].ArgTypes, textOID) + } + + got, err = cat.FindProcs("concat", nil) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || len(got[0].ArgTypes) != 2 { + t.Fatalf("FindProcs concat: got %+v", got) + } +} diff --git a/internal/core/schema.sql b/internal/core/schema.sql new file mode 100644 index 0000000000..e9d9741d4b --- /dev/null +++ b/internal/core/schema.sql @@ -0,0 +1,156 @@ +-- sql_namespace: schemas / namespaces +CREATE TABLE sql_namespace ( + oid INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE +); + +-- sql_dialect: registered SQL dialects (postgresql, sqlite, mysql, ...). +CREATE TABLE sql_dialect ( + oid INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE +); + +-- sql_dialect_flag: per-dialect configuration knobs (case-folding, +-- identifier quoting, alias scoping rules, etc.). Values are opaque +-- strings; the analyzer interprets them per key. +CREATE TABLE sql_dialect_flag ( + dialect_oid INTEGER NOT NULL REFERENCES sql_dialect(oid), + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (dialect_oid, key) +); + +-- sql_type: data types. Modeled on pg_type. +-- typtype: 'b'ase | 'c'omposite | 'd'omain | 'e'num | 'p'seudo | 'r'ange +-- category: 'N'umeric | 'S'tring | 'B'oolean | 'D'atetime | 'A'rray | +-- 'C'omposite | 'E'num | 'U'serdef | 'X'unknown +-- preferred: tie-breaker for implicit cast resolution within a category +-- element_oid: for arrays, points at the element type +-- dialect_oid: NULL = standard / shared across dialects +CREATE TABLE sql_type ( + oid INTEGER PRIMARY KEY AUTOINCREMENT, + namespace_oid INTEGER NOT NULL REFERENCES sql_namespace(oid), + dialect_oid INTEGER REFERENCES sql_dialect(oid), + name TEXT NOT NULL, + size INTEGER NOT NULL DEFAULT 0, + typtype TEXT NOT NULL DEFAULT 'b', + category TEXT, + preferred INTEGER NOT NULL DEFAULT 0, + element_oid INTEGER REFERENCES sql_type(oid), + UNIQUE (namespace_oid, name) +); +CREATE INDEX idx_sql_type_name ON sql_type(name); + +-- sql_class: relations (tables, views, indexes). +-- kind: 'r' = table, 'v' = view, 'i' = index, 'c' = composite type, 'f' = foreign +CREATE TABLE sql_class ( + oid INTEGER PRIMARY KEY AUTOINCREMENT, + namespace_oid INTEGER NOT NULL REFERENCES sql_namespace(oid), + name TEXT NOT NULL, + kind TEXT NOT NULL DEFAULT 'r', + UNIQUE(namespace_oid, name) +); + +-- sql_attribute: columns of a relation. +-- decl_type: original declared type string before normalization +-- (e.g. VARCHAR(10), BIGINT UNSIGNED, INTEGER PRIMARY KEY). +-- Useful for SQLite where multiple syntaxes collapse to +-- one of five affinities, and as a debugging aid. +-- type_length: length / precision (varchar(N), numeric(p,s).p, +-- char(N), bit(N)). 0 = unspecified. +-- type_scale: scale for numeric/decimal. 0 = unspecified. +-- auto_increment: rowid alias (sqlite INTEGER PRIMARY KEY), AUTOINCREMENT, +-- pg serial/bigserial/identity, mysql AUTO_INCREMENT. +-- is_primary_key: this column participates in the relation's primary key. +-- Set both for inline-column PK and for table-level PK. +-- is_unique: column has a UNIQUE constraint or a single-column UNIQUE +-- table constraint. +CREATE TABLE sql_attribute ( + oid INTEGER PRIMARY KEY AUTOINCREMENT, + class_oid INTEGER NOT NULL REFERENCES sql_class(oid), + name TEXT NOT NULL, + type_oid INTEGER NOT NULL REFERENCES sql_type(oid), + not_null INTEGER NOT NULL DEFAULT 0, + has_default INTEGER NOT NULL DEFAULT 0, + num INTEGER NOT NULL, -- ordinal position (1-based) + decl_type TEXT NOT NULL DEFAULT '', + type_length INTEGER NOT NULL DEFAULT 0, + type_scale INTEGER NOT NULL DEFAULT 0, + auto_increment INTEGER NOT NULL DEFAULT 0, + is_primary_key INTEGER NOT NULL DEFAULT 0, + is_unique INTEGER NOT NULL DEFAULT 0, + UNIQUE(class_oid, name), + UNIQUE(class_oid, num) +); + +-- sql_constraint: constraints on a relation. +-- kind: 'p' = primary key, 'f' = foreign key, 'u' = unique, 'c' = check +CREATE TABLE sql_constraint ( + oid INTEGER PRIMARY KEY AUTOINCREMENT, + class_oid INTEGER NOT NULL REFERENCES sql_class(oid), + name TEXT NOT NULL DEFAULT '', + kind TEXT NOT NULL, + columns TEXT NOT NULL DEFAULT '' -- comma-separated attribute nums +); + +-- sql_proc: functions, aggregates, window functions, procedures. +-- Modeled on pg_proc. +-- kind: 'f' = function, 'a' = aggregate, 'w' = window, 'p' = procedure +-- variadic_kind: 'n' = none, 'a' = array (VARIADIC any[]), 'v' = variadic-any +-- return_set: 1 if SETOF / table-returning +CREATE TABLE sql_proc ( + oid INTEGER PRIMARY KEY AUTOINCREMENT, + namespace_oid INTEGER REFERENCES sql_namespace(oid), + dialect_oid INTEGER REFERENCES sql_dialect(oid), + name TEXT NOT NULL, + kind TEXT NOT NULL DEFAULT 'f', + return_type_oid INTEGER NOT NULL REFERENCES sql_type(oid), + return_set INTEGER NOT NULL DEFAULT 0, + return_nullable INTEGER NOT NULL DEFAULT 1, + strict INTEGER NOT NULL DEFAULT 0, + variadic_kind TEXT NOT NULL DEFAULT 'n' +); + +-- sql_proc_arg: ordered argument list for a proc. +-- mode: 'i' = in, 'o' = out, 'b' = both, 't' = table, 'v' = variadic +CREATE TABLE sql_proc_arg ( + proc_oid INTEGER NOT NULL REFERENCES sql_proc(oid), + ord INTEGER NOT NULL, + name TEXT NOT NULL DEFAULT '', + type_oid INTEGER NOT NULL REFERENCES sql_type(oid), + mode TEXT NOT NULL DEFAULT 'i', + has_default INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (proc_oid, ord) +); + +-- sql_operator: operator overloads. +-- left_type_oid is NULL for prefix unary; right_type_oid is NULL for postfix. +CREATE TABLE sql_operator ( + oid INTEGER PRIMARY KEY AUTOINCREMENT, + namespace_oid INTEGER REFERENCES sql_namespace(oid), + dialect_oid INTEGER REFERENCES sql_dialect(oid), + name TEXT NOT NULL, + left_type_oid INTEGER REFERENCES sql_type(oid), + right_type_oid INTEGER REFERENCES sql_type(oid), + result_type_oid INTEGER NOT NULL REFERENCES sql_type(oid), + proc_oid INTEGER REFERENCES sql_proc(oid), + commutator_oid INTEGER REFERENCES sql_operator(oid), + negator_oid INTEGER REFERENCES sql_operator(oid) +); + +-- sql_cast: type coercion rules. +-- context: 'i' = implicit, 'a' = assignment-only, 'e' = explicit-only +-- proc_oid NULL = binary-coercible (no function needed) +CREATE TABLE sql_cast ( + source_type_oid INTEGER NOT NULL REFERENCES sql_type(oid), + target_type_oid INTEGER NOT NULL REFERENCES sql_type(oid), + proc_oid INTEGER REFERENCES sql_proc(oid), + context TEXT NOT NULL DEFAULT 'e', + dialect_oid INTEGER REFERENCES sql_dialect(oid), + PRIMARY KEY (source_type_oid, target_type_oid) +); + +-- Resolution-speed indexes. +CREATE INDEX idx_sql_proc_name ON sql_proc(name, namespace_oid); +CREATE INDEX idx_sql_operator_name ON sql_operator(name, left_type_oid, right_type_oid); +CREATE INDEX idx_sql_attribute_name ON sql_attribute(name); diff --git a/internal/core/types.go b/internal/core/types.go new file mode 100644 index 0000000000..6acc2de1c0 --- /dev/null +++ b/internal/core/types.go @@ -0,0 +1,137 @@ +package core + +import ( + "database/sql" + "fmt" + "strings" +) + +// TypeSpec describes a SQL type for insertion into the catalog. +// All fields are optional; Name is required. +type TypeSpec struct { + Name string + Size int + Typtype string // 'b'ase | 'c'omposite | 'd'omain | 'e'num | 'p'seudo | 'r'ange + Category string // 'N'umeric | 'S'tring | 'B'oolean | 'D'atetime | 'A'rray | ... + Preferred bool + NamespaceOID int64 // 0 = NULL + DialectOID int64 // 0 = NULL (shared) + ElementOID int64 // for arrays; 0 = NULL +} + +// CreateType inserts a base type (typtype='b') and returns its OID. +// For full control, use CreateTypeSpec. +func (c *Catalog) CreateType(name string, size int) (int64, error) { + return c.CreateTypeSpec(TypeSpec{Name: name, Size: size, Typtype: "b"}) +} + +// CreateTypeSpec inserts a type with full metadata. NamespaceOID defaults +// to the "public" namespace when zero so the simpler CreateType API and +// engines that don't track namespaces (sqlite) continue to work. +func (c *Catalog) CreateTypeSpec(t TypeSpec) (int64, error) { + if t.Typtype == "" { + t.Typtype = "b" + } + if t.NamespaceOID == 0 { + oid, err := c.NamespaceOID("public") + if err != nil { + return 0, fmt.Errorf("create type %q: default namespace: %w", t.Name, err) + } + t.NamespaceOID = oid + } + res, err := c.db.Exec( + `INSERT INTO sql_type + (name, size, typtype, category, preferred, namespace_oid, dialect_oid, element_oid) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + strings.ToLower(t.Name), t.Size, t.Typtype, + nullableString(t.Category), boolToInt(t.Preferred), + t.NamespaceOID, nullableOID(t.DialectOID), nullableOID(t.ElementOID), + ) + if err != nil { + return 0, fmt.Errorf("create type %q: %w", t.Name, err) + } + return res.LastInsertId() +} + +// TypeOID returns the OID for the given (unqualified) type name. When +// the same name lives in multiple namespaces (e.g. a system-internal +// duplicate in information_schema and pg_catalog), the lookup prefers +// pg_catalog, then public, then any other namespace by name. +func (c *Catalog) TypeOID(name string) (int64, error) { + var oid int64 + err := c.db.QueryRow( + `SELECT t.oid FROM sql_type t + JOIN sql_namespace ns ON ns.oid = t.namespace_oid + WHERE t.name = ? + ORDER BY CASE ns.name + WHEN 'pg_catalog' THEN 0 + WHEN 'public' THEN 1 + ELSE 2 END, + ns.name + LIMIT 1`, + strings.ToLower(name), + ).Scan(&oid) + if err != nil { + return 0, fmt.Errorf("type %q: %w", name, err) + } + return oid, nil +} + +// TypeName returns the name for the given type OID. +func (c *Catalog) TypeName(oid int64) (string, error) { + var name string + err := c.db.QueryRow(`SELECT name FROM sql_type WHERE oid = ?`, oid).Scan(&name) + if err != nil { + return "", fmt.Errorf("type oid %d: %w", oid, err) + } + return name, nil +} + +// TypeInfo bundles the metadata returned by LookupType. +type TypeInfo struct { + OID int64 + Name string + Category string + Typtype string + Preferred bool +} + +// LookupType returns metadata for the given type OID. +func (c *Catalog) LookupType(oid int64) (TypeInfo, error) { + var ( + ti TypeInfo + category sql.NullString + pref int + ) + err := c.db.QueryRow( + `SELECT oid, name, COALESCE(category, ''), typtype, preferred FROM sql_type WHERE oid = ?`, + oid, + ).Scan(&ti.OID, &ti.Name, &category, &ti.Typtype, &pref) + if err != nil { + return ti, fmt.Errorf("lookup type oid %d: %w", oid, err) + } + ti.Category = category.String + ti.Preferred = pref != 0 + return ti, nil +} + +func nullableOID(oid int64) any { + if oid == 0 { + return nil + } + return oid +} + +func nullableString(s string) any { + if s == "" { + return nil + } + return s +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} From 071254eb28e8b5beccaef8ca17c7318550a1c082 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 22 Jul 2026 16:05:40 +0000 Subject: [PATCH 2/7] clickhouse: analyze queries through the core catalog + analyzer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire ClickHouse onto the merged core: a dialect seed registering the built-in ClickHouse types, and a DDL handler that populates the core catalog from CREATE TABLE using sqlc's existing ClickHouse parser. A smoke test proves the full vertical path — ClickHouse SQL -> sqlc's ClickHouse parser -> internal/sql/ast -> core catalog + analyzer -> PrepareResult — resolving column names, types, nullability, source bindings, and star expansion, with none of the legacy compiler analyze step involved. Also fix the ClickHouse converter to render nested type parameters (the inner type of Nullable(T)/Array(T), Decimal precision, etc.) into TypeName.Name instead of dropping them as TODO nodes, so wrapped types resolve to their effective scalar type. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XTGxNHW6v1S1YyC9FDSgrK --- internal/engine/clickhouse/analyze_test.go | 100 +++++++++ internal/engine/clickhouse/convert.go | 52 ++++- internal/engine/clickhouse/schema.go | 227 +++++++++++++++++++++ internal/engine/clickhouse/seed.go | 79 +++++++ 4 files changed, 450 insertions(+), 8 deletions(-) create mode 100644 internal/engine/clickhouse/analyze_test.go create mode 100644 internal/engine/clickhouse/schema.go create mode 100644 internal/engine/clickhouse/seed.go diff --git a/internal/engine/clickhouse/analyze_test.go b/internal/engine/clickhouse/analyze_test.go new file mode 100644 index 0000000000..8d0369cf59 --- /dev/null +++ b/internal/engine/clickhouse/analyze_test.go @@ -0,0 +1,100 @@ +package clickhouse_test + +import ( + "strings" + "testing" + + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/core/analyzer" + "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" +) + +// analyzeOne seeds a ClickHouse-dialect catalog, applies ddl, then runs +// the dialect-neutral core analyzer over the trailing query. It proves +// the full ClickHouse vertical path: ClickHouse SQL -> sqlc's ClickHouse +// parser -> internal/sql/ast -> core catalog + analyzer -> PrepareResult, +// with no legacy compiler analyze step involved. +func analyzeOne(t *testing.T, ddl, query string) core.PrepareResult { + t.Helper() + cat, err := core.New(clickhouse.Dialect()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { cat.Close() }) + + if err := clickhouse.LoadSchema(cat, ddl); err != nil { + t.Fatalf("load schema: %v", err) + } + + stmts, err := clickhouse.NewParser().Parse(strings.NewReader(query)) + if err != nil { + t.Fatalf("parse query: %v", err) + } + if len(stmts) != 1 { + t.Fatalf("expected 1 stmt, got %d", len(stmts)) + } + res, err := analyzer.Prepare(cat, stmts[0].Raw) + if err != nil { + t.Fatalf("analyze: %v", err) + } + return res +} + +func colByName(res core.PrepareResult, name string) (core.Column, bool) { + for _, c := range res.Columns { + if c.Name == name { + return c, true + } + } + return core.Column{}, false +} + +const eventsDDL = ` +CREATE TABLE events ( + id UInt64, + name String, + tag Nullable(String), + amount Decimal(18, 4) +) ENGINE = MergeTree ORDER BY id +` + +func TestClickHouseSelectColumns(t *testing.T) { + res := analyzeOne(t, eventsDDL, `SELECT id, name, tag FROM events`) + + if len(res.Columns) != 3 { + t.Fatalf("got %d cols, want 3: %+v", len(res.Columns), res.Columns) + } + + id, ok := colByName(res, "id") + if !ok || id.DataType != "uint64" || !id.NotNull { + t.Errorf("id: %+v (ok=%v)", id, ok) + } + name, ok := colByName(res, "name") + if !ok || name.DataType != "string" || !name.NotNull { + t.Errorf("name: %+v (ok=%v)", name, ok) + } + // Nullable(String) must resolve to the inner scalar and be nullable. + tag, ok := colByName(res, "tag") + if !ok || tag.DataType != "string" || tag.NotNull { + t.Errorf("tag: want string/nullable, got %+v (ok=%v)", tag, ok) + } + + for _, c := range res.Columns { + if c.SourceClassOID == 0 || c.SourceAttributeOID == 0 { + t.Errorf("col %s missing source binding: %+v", c.Name, c) + } + } +} + +func TestClickHouseSelectStar(t *testing.T) { + res := analyzeOne(t, eventsDDL, `SELECT * FROM events`) + if len(res.Columns) != 4 { + t.Fatalf("got %d cols, want 4: %+v", len(res.Columns), res.Columns) + } + want := []string{"id", "name", "tag", "amount"} + for i, w := range want { + if res.Columns[i].Name != w { + t.Errorf("col %d: got %q, want %q", i, res.Columns[i].Name, w) + } + } +} diff --git a/internal/engine/clickhouse/convert.go b/internal/engine/clickhouse/convert.go index ba2817e2bb..f1fad62be7 100644 --- a/internal/engine/clickhouse/convert.go +++ b/internal/engine/clickhouse/convert.go @@ -1,6 +1,7 @@ package clickhouse import ( + "fmt" "strconv" "strings" @@ -825,15 +826,14 @@ func (c *cc) convertColumnDeclaration(n *chast.ColumnDeclaration) *ast.ColumnDef } if n.Type != nil { + // Render the full type text (including nested type parameters such + // as the inner type of Nullable(T) / Array(T) and the precision of + // Decimal(P, S)) into Name so downstream consumers can recover the + // complete declaration. Nested type parameters are themselves + // *chast.DataType nodes, which the generic expression converter + // cannot represent, so they are rendered here instead. colDef.TypeName = &ast.TypeName{ - Name: n.Type.Name, - } - // Handle type parameters (e.g., Decimal(10, 2)) - if len(n.Type.Parameters) > 0 { - colDef.TypeName.Typmods = &ast.List{} - for _, param := range n.Type.Parameters { - colDef.TypeName.Typmods.Items = append(colDef.TypeName.Typmods.Items, c.convertExpr(param)) - } + Name: renderDataType(n.Type), } } @@ -855,6 +855,42 @@ func (c *cc) convertColumnDeclaration(n *chast.ColumnDeclaration) *ast.ColumnDef return colDef } +// renderDataType renders a ClickHouse type node back to its canonical +// textual form, e.g. Nullable(String), Array(UInt64), Decimal(18, 4), or +// Map(String, Array(Nullable(UInt32))). Nested type parameters recurse. +func renderDataType(dt *chast.DataType) string { + if dt == nil { + return "" + } + if len(dt.Parameters) == 0 { + return dt.Name + } + parts := make([]string, 0, len(dt.Parameters)) + for _, p := range dt.Parameters { + parts = append(parts, renderTypeParam(p)) + } + return dt.Name + "(" + strings.Join(parts, ", ") + ")" +} + +// renderTypeParam renders a single type parameter: a nested type, a +// numeric/string literal (Decimal precision, FixedString length, Enum +// value), or an identifier. +func renderTypeParam(e chast.Expression) string { + switch v := e.(type) { + case *chast.DataType: + return renderDataType(v) + case *chast.Literal: + if v.Source != "" { + return v.Source + } + return fmt.Sprintf("%v", v.Value) + case *chast.Identifier: + return strings.Join(v.Parts, ".") + default: + return "" + } +} + func (c *cc) convertUpdateQuery(n *chast.UpdateQuery) *ast.UpdateStmt { rv := &ast.RangeVar{ Relname: &n.Table, diff --git a/internal/engine/clickhouse/schema.go b/internal/engine/clickhouse/schema.go new file mode 100644 index 0000000000..283d71e70a --- /dev/null +++ b/internal/engine/clickhouse/schema.go @@ -0,0 +1,227 @@ +package clickhouse + +import ( + "fmt" + "strings" + + "github.com/sqlc-dev/sqlc/internal/core" + "github.com/sqlc-dev/sqlc/internal/sql/ast" +) + +// LoadSchema parses ClickHouse DDL and applies every schema-altering +// statement to the core catalog. It is the ClickHouse entry point for +// populating a catalog from migration files. +func LoadSchema(cat *core.Catalog, ddl string) error { + stmts, err := NewParser().Parse(strings.NewReader(ddl)) + if err != nil { + return fmt.Errorf("clickhouse: parse schema: %w", err) + } + for _, s := range stmts { + if err := Apply(cat, s.Raw); err != nil { + return err + } + } + return nil +} + +// Apply walks one parsed ClickHouse statement and applies it to the +// catalog when it is schema-altering (CREATE TABLE, DROP TABLE). +// Non-DDL and unsupported DDL forms are ignored so a mixed DDL/DML +// stream can be handed to the catalog without pre-filtering. +func Apply(cat *core.Catalog, n ast.Node) error { + switch v := n.(type) { + case nil: + return nil + case *ast.RawStmt: + return Apply(cat, v.Stmt) + case *ast.List: + for _, it := range v.Items { + if err := Apply(cat, it); err != nil { + return err + } + } + return nil + case *ast.CreateTableStmt: + return applyCreateTable(cat, v) + case *ast.DropTableStmt: + return applyDropTable(cat, v) + } + return nil +} + +func applyCreateTable(cat *core.Catalog, stmt *ast.CreateTableStmt) error { + if stmt.Name == nil { + return fmt.Errorf("clickhouse: create table with nil name") + } + nsOID, err := resolveOrCreateNamespace(cat, stmt.Name.Schema) + if err != nil { + return err + } + if _, err := cat.ClassOID(nsOID, stmt.Name.Name); err == nil { + if stmt.IfNotExists { + return nil + } + return fmt.Errorf("clickhouse: relation %q already exists", stmt.Name.Name) + } + classOID, err := cat.CreateClass(nsOID, stmt.Name.Name, "r") + if err != nil { + return err + } + for i, col := range stmt.Cols { + if col == nil || col.TypeName == nil { + return fmt.Errorf("clickhouse: column %d on %q: missing type", i+1, stmt.Name.Name) + } + // TODO: the core catalog does not yet model array-ness on an + // attribute; for now an Array(T) column is stored as its element + // type T. Preserving the array wrapper is a follow-up. + typeName, _, _ := unwrapType(col.TypeName) + typeOID, err := resolveOrCreateType(cat, typeName) + if err != nil { + return fmt.Errorf("clickhouse: column %s.%s: %w", stmt.Name.Name, col.Colname, err) + } + if err := cat.CreateAttributeSpec(core.AttributeSpec{ + ClassOID: classOID, + Name: col.Colname, + TypeOID: typeOID, + Num: i + 1, + NotNull: col.IsNotNull || col.PrimaryKey, + IsPrimaryKey: col.PrimaryKey, + DeclType: col.TypeName.Name, + }); err != nil { + return fmt.Errorf("clickhouse: attr %s.%s: %w", stmt.Name.Name, col.Colname, err) + } + } + return nil +} + +func applyDropTable(cat *core.Catalog, stmt *ast.DropTableStmt) error { + for _, tn := range stmt.Tables { + if tn == nil { + continue + } + nsOID, err := cat.NamespaceOID(nsName(tn.Schema)) + if err != nil { + if stmt.IfExists { + continue + } + return err + } + classOID, err := cat.ClassOID(nsOID, tn.Name) + if err != nil { + if stmt.IfExists { + continue + } + return fmt.Errorf("clickhouse: drop table %q: %w", tn.Name, err) + } + db := cat.DB() + if _, err := db.Exec(`DELETE FROM sql_attribute WHERE class_oid = ?`, classOID); err != nil { + return err + } + if _, err := db.Exec(`DELETE FROM sql_class WHERE oid = ?`, classOID); err != nil { + return err + } + } + return nil +} + +// nsName maps an empty schema to the catalog's default namespace. The +// analyzer resolves unqualified table references against "public", so +// ClickHouse's "default" database is mapped onto it for now. Wiring a +// per-dialect default namespace is a follow-up. +func nsName(schema string) string { + if schema == "" { + return "public" + } + return schema +} + +func resolveOrCreateNamespace(cat *core.Catalog, schema string) (int64, error) { + name := nsName(schema) + if oid, err := cat.NamespaceOID(name); err == nil { + return oid, nil + } + return cat.CreateNamespace(name) +} + +// resolveOrCreateType looks a type up by name, registering it as an +// unknown user type if the seed did not provide it (e.g. an Enum with +// inline variants, or a type name we have not catalogued yet). +func resolveOrCreateType(cat *core.Catalog, name string) (int64, error) { + canonical := strings.ToLower(strings.TrimSpace(name)) + if canonical == "" { + canonical = "nothing" + } + if oid, err := cat.TypeOID(canonical); err == nil { + return oid, nil + } + return cat.CreateType(canonical, 0) +} + +// unwrapType reduces a ClickHouse column type to the effective scalar +// type used for storage, reporting whether it is an array and whether it +// is nullable. The ClickHouse converter renders the full type text into +// TypeName.Name (e.g. "Nullable(Array(UInt64))"), which is parsed here. +// It unwraps any nesting of Nullable / LowCardinality / Array; the +// remaining scalar (String, UInt64, Decimal(18, 4), ...) is returned as +// its bare type name, lower-cased. +func unwrapType(tn *ast.TypeName) (name string, isArray, nullable bool) { + return unwrapTypeString(tn.Name) +} + +func unwrapTypeString(s string) (name string, isArray, nullable bool) { + base, args := splitType(s) + switch strings.ToLower(base) { + case "nullable": + if len(args) == 1 { + inner, arr, _ := unwrapTypeString(args[0]) + return inner, arr, true + } + return strings.ToLower(base), false, true + case "lowcardinality": + if len(args) == 1 { + return unwrapTypeString(args[0]) + } + return strings.ToLower(base), false, false + case "array": + if len(args) == 1 { + inner, _, nul := unwrapTypeString(args[0]) + return inner, true, nul + } + return strings.ToLower(base), true, false + default: + // Scalar type. Drop any (length/precision) modifiers for the stored + // type name; capturing them into TypeLength/TypeScale is a follow-up. + return strings.ToLower(base), false, false + } +} + +// splitType breaks "Name(arg1, arg2, ...)" into its base name and +// top-level arguments, respecting nested parentheses. A type with no +// parentheses returns (name, nil). +func splitType(s string) (base string, args []string) { + s = strings.TrimSpace(s) + open := strings.IndexByte(s, '(') + if open < 0 || !strings.HasSuffix(s, ")") { + return s, nil + } + base = strings.TrimSpace(s[:open]) + inner := s[open+1 : len(s)-1] + depth, start := 0, 0 + for i := 0; i < len(inner); i++ { + switch inner[i] { + case '(': + depth++ + case ')': + depth-- + case ',': + if depth == 0 { + args = append(args, strings.TrimSpace(inner[start:i])) + start = i + 1 + } + } + } + if last := strings.TrimSpace(inner[start:]); last != "" { + args = append(args, last) + } + return base, args +} diff --git a/internal/engine/clickhouse/seed.go b/internal/engine/clickhouse/seed.go new file mode 100644 index 0000000000..af7d149875 --- /dev/null +++ b/internal/engine/clickhouse/seed.go @@ -0,0 +1,79 @@ +package clickhouse + +import ( + "fmt" + + "github.com/sqlc-dev/sqlc/internal/core" +) + +// Dialect returns a core.Option that registers the "clickhouse" dialect +// and seeds the catalog with the built-in ClickHouse data types. It is +// the ClickHouse counterpart to the per-dialect Dialect() helpers in +// xqlc, and is how a ClickHouse-backed core.Catalog is constructed: +// +// cat, err := core.New(clickhouse.Dialect()) +func Dialect() core.Option { + return core.WithSeed(Seed) +} + +// chType pairs a ClickHouse type name with its pg_type-style category so +// the catalog can reason about it during (future) operator and cast +// resolution. Names are stored lower-cased by the catalog. +type chType struct { + name string + category string // 'N'umeric | 'S'tring | 'B'oolean | 'D'atetime | 'A'rray | 'U'serdef +} + +// clickhouseTypes is the built-in type surface. It is intentionally a +// curated hand list for now; once testgen/clickhouse exists these will +// be regenerated against a real ClickHouse instance, matching the +// seedgen approach used by the other engines. +var clickhouseTypes = []chType{ + // Unsigned integers + {"UInt8", "N"}, {"UInt16", "N"}, {"UInt32", "N"}, {"UInt64", "N"}, + {"UInt128", "N"}, {"UInt256", "N"}, + // Signed integers + {"Int8", "N"}, {"Int16", "N"}, {"Int32", "N"}, {"Int64", "N"}, + {"Int128", "N"}, {"Int256", "N"}, + // Floating point / decimal + {"Float32", "N"}, {"Float64", "N"}, {"BFloat16", "N"}, + {"Decimal", "N"}, {"Decimal32", "N"}, {"Decimal64", "N"}, + {"Decimal128", "N"}, {"Decimal256", "N"}, + // Boolean + {"Bool", "B"}, + // Strings / binary + {"String", "S"}, {"FixedString", "S"}, {"UUID", "S"}, + // Date & time + {"Date", "D"}, {"Date32", "D"}, {"DateTime", "D"}, {"DateTime64", "D"}, + // Network / misc scalar + {"IPv4", "S"}, {"IPv6", "S"}, {"JSON", "U"}, + // Enums are declared with their variants; the base names still resolve. + {"Enum8", "U"}, {"Enum16", "U"}, + // Wrapper / composite type names. Columns declared with these are + // normally unwrapped to their inner scalar by the DDL handler, but we + // register the names so an un-unwrappable declaration still resolves. + {"Nullable", "U"}, {"LowCardinality", "U"}, {"Array", "A"}, + {"Map", "U"}, {"Tuple", "U"}, {"Nested", "U"}, {"Nothing", "U"}, +} + +// Seed registers the ClickHouse dialect row and its built-in types on a +// freshly bootstrapped catalog. +func Seed(cat *core.Catalog) error { + dialectOID, err := cat.CreateDialect("clickhouse") + if err != nil { + return err + } + // ClickHouse identifiers are case-sensitive, so unlike MySQL/SQLite we + // do not set a case-folding flag. + for _, t := range clickhouseTypes { + if _, err := cat.CreateTypeSpec(core.TypeSpec{ + Name: t.name, + Typtype: "b", + Category: t.category, + DialectOID: dialectOID, + }); err != nil { + return fmt.Errorf("seed clickhouse type %q: %w", t.name, err) + } + } + return nil +} From fda4d41588fe124208ae19492321e13b46d8bd8c Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 22 Jul 2026 16:31:31 +0000 Subject: [PATCH 3/7] clickhouse: wire end-to-end sqlc generate onto the core Add the EngineClickHouse engine and a dedicated compile path so `sqlc generate` produces Go for ClickHouse entirely through the xqlc core, bypassing the legacy compiler analyze step and the in-memory sql/catalog: - config: add the "clickhouse" engine constant. - compiler: NewCompiler builds a core.Catalog seeded with the ClickHouse dialect; parseCatalog applies schema DDL to it; a new parseQueryCore resolves each query's columns and parameters via core/analyzer and assembles *compiler.Query, reusing only the shared query-metadata parsing. The legacy analyzeQuery/inferQuery/outputColumns path and the analyzer.Analyzer seam are never entered. - codegen: project the core catalog into plugin.Catalog for model/enum generation, and add a ClickHouse -> Go type map (Nullable(T) -> *T, the integer ladder, Float32/64, String, DateTime -> time.Time, ...). - clickhouse parser: compute statement byte-spans with a running offset and a semicolon scan (doubleclick reports statement starts but not ends), so leading "-- name:" annotations fall inside each statement. An endtoend case (clickhouse_select) exercises the full pipeline and its golden Go output is committed. Updating parse_basic/clickhouse's golden reflects the corrected statement spans and now-detected query name/cmd. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XTGxNHW6v1S1YyC9FDSgrK --- internal/cmd/shim.go | 83 +++++++++++- internal/codegen/golang/clickhouse_type.go | 68 ++++++++++ internal/codegen/golang/go_type.go | 2 + internal/compiler/compile.go | 18 ++- internal/compiler/engine.go | 21 +++ internal/compiler/parse.go | 6 + internal/compiler/parse_clickhouse.go | 126 ++++++++++++++++++ internal/compiler/result.go | 7 + internal/config/config.go | 1 + internal/core/analyzer/scope.go | 4 +- internal/core/attribute.go | 70 +++++----- internal/core/cast_test.go | 16 +-- .../clickhouse_select/clickhouse/db/db.go | 31 +++++ .../clickhouse_select/clickhouse/db/models.go | 5 + .../clickhouse/db/query.sql.go | 52 ++++++++ .../clickhouse_select/clickhouse/query.sql | 2 + .../clickhouse_select/clickhouse/schema.sql | 7 + .../clickhouse_select/clickhouse/sqlc.json | 16 +++ .../parse_basic/clickhouse/stdout.txt | 6 +- internal/engine/clickhouse/convert.go | 4 +- internal/engine/clickhouse/parse.go | 94 +++++++++++-- 21 files changed, 577 insertions(+), 62 deletions(-) create mode 100644 internal/codegen/golang/clickhouse_type.go create mode 100644 internal/compiler/parse_clickhouse.go create mode 100644 internal/endtoend/testdata/clickhouse_select/clickhouse/db/db.go create mode 100644 internal/endtoend/testdata/clickhouse_select/clickhouse/db/models.go create mode 100644 internal/endtoend/testdata/clickhouse_select/clickhouse/db/query.sql.go create mode 100644 internal/endtoend/testdata/clickhouse_select/clickhouse/query.sql create mode 100644 internal/endtoend/testdata/clickhouse_select/clickhouse/schema.sql create mode 100644 internal/endtoend/testdata/clickhouse_select/clickhouse/sqlc.json diff --git a/internal/cmd/shim.go b/internal/cmd/shim.go index 654500429a..690364eee9 100644 --- a/internal/cmd/shim.go +++ b/internal/cmd/shim.go @@ -4,6 +4,7 @@ import ( "github.com/sqlc-dev/sqlc/internal/compiler" "github.com/sqlc-dev/sqlc/internal/config" "github.com/sqlc-dev/sqlc/internal/config/convert" + "github.com/sqlc-dev/sqlc/internal/core" "github.com/sqlc-dev/sqlc/internal/info" "github.com/sqlc-dev/sqlc/internal/plugin" "github.com/sqlc-dev/sqlc/internal/sql/catalog" @@ -224,10 +225,90 @@ func pluginQueryParam(p compiler.Parameter) *plugin.Parameter { } func codeGenRequest(r *compiler.Result, settings config.CombinedSettings) *plugin.GenerateRequest { + // Engines on the xqlc core (ClickHouse) project the codegen catalog + // from the core catalog rather than the in-memory sql/catalog, which is + // nil on that path. + var cat *plugin.Catalog + if r.CoreCatalog != nil { + cat = pluginCatalogFromCore(r.CoreCatalog) + } else { + cat = pluginCatalog(r.Catalog) + } return &plugin.GenerateRequest{ Settings: pluginSettings(r, settings), - Catalog: pluginCatalog(r.Catalog), + Catalog: cat, Queries: pluginQueries(r), SqlcVersion: info.Version, } } + +// pluginCatalogFromCore projects a core.Catalog (the xqlc SQLite-backed +// catalog) into the plugin.Catalog that codegen consumes to emit models +// and enums. It reads the namespace / class / attribute / type tables +// directly. Projection is best-effort: an unexpected query error against +// the in-memory catalog yields a partial catalog rather than aborting +// generation. +func pluginCatalogFromCore(cc *core.Catalog) *plugin.Catalog { + db := cc.DB() + var schemas []*plugin.Schema + + type row struct { + oid int64 + name string + } + readRows := func(query string, args ...any) []row { + rows, err := db.Query(query, args...) + if err != nil { + return nil + } + defer rows.Close() + var out []row + for rows.Next() { + var r row + if err := rows.Scan(&r.oid, &r.name); err != nil { + return out + } + out = append(out, r) + } + return out + } + + for _, ns := range readRows(`SELECT oid, name FROM sql_namespace ORDER BY oid`) { + var tables []*plugin.Table + for _, cl := range readRows( + `SELECT oid, name FROM sql_class WHERE namespace_oid = ? AND kind = 'r' ORDER BY oid`, ns.oid, + ) { + rel := &plugin.Identifier{Schema: ns.name, Name: cl.name} + var columns []*plugin.Column + crows, err := db.Query( + `SELECT a.name, t.name, a.not_null + FROM sql_attribute a JOIN sql_type t ON t.oid = a.type_oid + WHERE a.class_oid = ? ORDER BY a.num`, cl.oid, + ) + if err != nil { + continue + } + for crows.Next() { + var name, typeName string + var notNull int + if err := crows.Scan(&name, &typeName, ¬Null); err != nil { + break + } + columns = append(columns, &plugin.Column{ + Name: name, + Type: &plugin.Identifier{Name: typeName}, + NotNull: notNull != 0, + Table: rel, + }) + } + crows.Close() + tables = append(tables, &plugin.Table{Rel: rel, Columns: columns}) + } + schemas = append(schemas, &plugin.Schema{Name: ns.name, Tables: tables}) + } + + return &plugin.Catalog{ + DefaultSchema: "public", + Schemas: schemas, + } +} diff --git a/internal/codegen/golang/clickhouse_type.go b/internal/codegen/golang/clickhouse_type.go new file mode 100644 index 0000000000..93613eec06 --- /dev/null +++ b/internal/codegen/golang/clickhouse_type.go @@ -0,0 +1,68 @@ +package golang + +import ( + "strings" + + "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts" + "github.com/sqlc-dev/sqlc/internal/codegen/sdk" + "github.com/sqlc-dev/sqlc/internal/plugin" +) + +// clickhouseType maps a ClickHouse column type to a Go type. Type names +// arrive lower-cased from the core catalog (e.g. "uint64", "string", +// "datetime"). Nullable columns (NotNull == false) map to a pointer, which +// is how the clickhouse-go driver represents Nullable(T). +func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string { + dt := strings.ToLower(sdk.DataType(col.Type)) + notNull := col.NotNull + + switch dt { + case "uint8": + return nullable(notNull, "uint8") + case "uint16": + return nullable(notNull, "uint16") + case "uint32": + return nullable(notNull, "uint32") + case "uint64": + return nullable(notNull, "uint64") + case "int8": + return nullable(notNull, "int8") + case "int16": + return nullable(notNull, "int16") + case "int32": + return nullable(notNull, "int32") + case "int64": + return nullable(notNull, "int64") + case "uint128", "uint256", "int128", "int256": + // Big integers are represented as *big.Int by clickhouse-go. + return "*big.Int" + case "float32", "bfloat16": + return nullable(notNull, "float32") + case "float64": + return nullable(notNull, "float64") + case "bool": + return nullable(notNull, "bool") + case "string", "fixedstring": + return nullable(notNull, "string") + case "date", "date32", "datetime", "datetime64": + return nullable(notNull, "time.Time") + + // The following resolve to string for now; richer mappings + // (decimal.Decimal, uuid.UUID, netip.Addr, json.RawMessage) require + // wiring their imports into the Go importer and are a follow-up. + case "decimal", "decimal32", "decimal64", "decimal128", "decimal256", + "uuid", "ipv4", "ipv6", "json", "enum8", "enum16": + return nullable(notNull, "string") + + default: + return "interface{}" + } +} + +// nullable wraps a base Go type in a pointer when the column is nullable. +func nullable(notNull bool, base string) string { + if notNull { + return base + } + return "*" + base +} diff --git a/internal/codegen/golang/go_type.go b/internal/codegen/golang/go_type.go index 21914736d0..b203dd525d 100644 --- a/internal/codegen/golang/go_type.go +++ b/internal/codegen/golang/go_type.go @@ -86,6 +86,8 @@ func goInnerType(req *plugin.GenerateRequest, options *opts.Options, col *plugin return postgresType(req, options, col) case "sqlite": return sqliteType(req, options, col) + case "clickhouse": + return clickhouseType(req, options, col) default: return "interface{}" } diff --git a/internal/compiler/compile.go b/internal/compiler/compile.go index b6bba42e16..b5665c28ef 100644 --- a/internal/compiler/compile.go +++ b/internal/compiler/compile.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" "github.com/sqlc-dev/sqlc/internal/migrations" "github.com/sqlc-dev/sqlc/internal/multierr" "github.com/sqlc-dev/sqlc/internal/opts" @@ -55,6 +56,18 @@ func (c *Compiler) parseCatalog(schemas []string) error { continue } + // ClickHouse populates the core catalog instead of the in-memory + // sql/catalog. + if c.coreCatalog != nil { + for i := range stmts { + if err := clickhouse.Apply(c.coreCatalog, stmts[i].Raw); err != nil { + merr.Add(filename, contents, stmts[i].Pos(), err) + continue + } + } + continue + } + for i := range stmts { if err := c.catalog.Update(stmts[i], c); err != nil { merr.Add(filename, contents, stmts[i].Pos(), err) @@ -135,7 +148,8 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) { } return &Result{ - Catalog: c.catalog, - Queries: q, + Catalog: c.catalog, + CoreCatalog: c.coreCatalog, + Queries: q, }, nil } diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index 64fdf3d5c7..43767b0faf 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -6,7 +6,9 @@ import ( "github.com/sqlc-dev/sqlc/internal/analyzer" "github.com/sqlc-dev/sqlc/internal/config" + "github.com/sqlc-dev/sqlc/internal/core" "github.com/sqlc-dev/sqlc/internal/dbmanager" + "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" "github.com/sqlc-dev/sqlc/internal/engine/dolphin" "github.com/sqlc-dev/sqlc/internal/engine/postgresql" pganalyze "github.com/sqlc-dev/sqlc/internal/engine/postgresql/analyzer" @@ -27,6 +29,11 @@ type Compiler struct { client dbmanager.Client selector selector + // coreCatalog is the xqlc-derived catalog used by engines whose + // analysis runs on the core analyzer (currently ClickHouse) instead of + // the legacy compiler analyze step. It is nil for other engines. + coreCatalog *core.Catalog + schema []string // databaseOnlyMode indicates that the compiler should use database-only analysis @@ -111,6 +118,17 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts ) } } + case config.EngineClickHouse: + // ClickHouse runs on the xqlc analysis core: its schema and queries + // are resolved against a core.Catalog by the core analyzer, not the + // legacy compiler analyze step or the in-memory sql/catalog. + c.parser = clickhouse.NewParser() + c.selector = newDefaultSelector() + cat, err := core.New(clickhouse.Dialect()) + if err != nil { + return nil, fmt.Errorf("clickhouse: init catalog: %w", err) + } + c.coreCatalog = cat default: return nil, fmt.Errorf("unknown engine: %s", conf.Engine) } @@ -145,4 +163,7 @@ func (c *Compiler) Close(ctx context.Context) { if c.client != nil { c.client.Close(ctx) } + if c.coreCatalog != nil { + c.coreCatalog.Close() + } } diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go index 2f9afb72c1..61b79ec1f7 100644 --- a/internal/compiler/parse.go +++ b/internal/compiler/parse.go @@ -19,6 +19,12 @@ import ( var debugDumpAST = sqlcdebug.New("dumpast") func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, error) { + // ClickHouse resolves types through the core analyzer, entirely + // bypassing the legacy analyze step below. + if c.coreCatalog != nil { + return c.parseQueryCore(stmt, src) + } + ctx := context.Background() if debugDumpAST.Value() == "1" { diff --git a/internal/compiler/parse_clickhouse.go b/internal/compiler/parse_clickhouse.go new file mode 100644 index 0000000000..f10466b764 --- /dev/null +++ b/internal/compiler/parse_clickhouse.go @@ -0,0 +1,126 @@ +package compiler + +import ( + "errors" + "strings" + + "github.com/sqlc-dev/sqlc/internal/core" + coreanalyzer "github.com/sqlc-dev/sqlc/internal/core/analyzer" + "github.com/sqlc-dev/sqlc/internal/metadata" + "github.com/sqlc-dev/sqlc/internal/source" + "github.com/sqlc-dev/sqlc/internal/sql/ast" + "github.com/sqlc-dev/sqlc/internal/sql/validate" +) + +// parseQueryCore is the analysis path for engines backed by the xqlc core +// catalog and analyzer (currently ClickHouse). It reuses the shared, +// engine-agnostic query-metadata parsing but resolves columns and +// parameters through core/analyzer, never touching the legacy compiler +// analyze step (inferQuery/outputColumns/parameters) or the +// analyzer.Analyzer seam. +func (c *Compiler) parseQueryCore(stmt ast.Node, src string) (*Query, error) { + raw, ok := stmt.(*ast.RawStmt) + if !ok { + return nil, errors.New("node is not a statement") + } + rawSQL, err := source.Pluck(src, raw.StmtLocation, raw.StmtLen) + if err != nil { + return nil, err + } + if strings.TrimSpace(rawSQL) == "" { + return nil, errors.New("missing semicolon at end of file") + } + + name, cmd, err := metadata.ParseQueryNameAndType(rawSQL, metadata.CommentSyntax(c.parser.CommentSyntax())) + if err != nil { + return nil, err + } + if name == "" { + return nil, nil + } + if err := validate.Cmd(raw.Stmt, name, cmd); err != nil { + return nil, err + } + + md := metadata.Metadata{Name: name, Cmd: cmd} + cleanedComments, err := source.CleanedComments(rawSQL, c.parser.CommentSyntax()) + if err != nil { + return nil, err + } + md.Params, md.Flags, md.RuleSkiplist, err = metadata.ParseCommentFlags(cleanedComments) + if err != nil { + return nil, err + } + + var cols []*Column + var params []Parameter + // Only result-shaped statements produce columns/parameters. Non-SELECT + // statements (:exec, DDL) currently yield an empty shape; growing the + // core analyzer to cover INSERT/UPDATE/DELETE is a follow-up. + if _, ok := raw.Stmt.(*ast.SelectStmt); ok { + res, err := coreanalyzer.Prepare(c.coreCatalog, raw) + if err != nil { + return nil, err + } + for _, col := range res.Columns { + cols = append(cols, coreColumn(col)) + } + for _, p := range res.Parameters { + params = append(params, Parameter{Number: p.Number, Column: coreParamColumn(p)}) + } + } + + trimmed, comments, err := source.StripComments(rawSQL) + if err != nil { + return nil, err + } + md.Comments = comments + + var insertTable *ast.TableName + if ins, ok := raw.Stmt.(*ast.InsertStmt); ok { + insertTable, _ = ParseTableName(ins.Relation) + } + + return &Query{ + RawStmt: raw, + Metadata: md, + Params: params, + Columns: cols, + SQL: trimmed, + InsertIntoTable: insertTable, + }, nil +} + +// coreColumn maps a core.Column (analyzer output) onto the compiler's +// Column. The type name is carried in DataType; Type is left nil so the +// codegen shim uses DataType directly. +func coreColumn(c core.Column) *Column { + col := &Column{ + Name: c.Name, + DataType: c.DataType, + NotNull: c.NotNull, + } + if c.Source != nil && c.Source.Table != "" { + col.Table = &ast.TableName{Schema: c.Source.Schema, Name: c.Source.Table} + col.TableAlias = c.Source.TableAlias + col.OriginalName = c.Source.Column + } + if c.TypeLength > 0 { + l := c.TypeLength + col.Length = &l + } + return col +} + +func coreParamColumn(p core.Parameter) *Column { + col := &Column{ + Name: p.Name, + DataType: p.DataType, + NotNull: p.NotNull, + } + if p.Source != nil && p.Source.Table != "" { + col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table} + col.OriginalName = p.Source.Column + } + return col +} diff --git a/internal/compiler/result.go b/internal/compiler/result.go index 3647da630f..e77933d555 100644 --- a/internal/compiler/result.go +++ b/internal/compiler/result.go @@ -1,10 +1,17 @@ package compiler import ( + "github.com/sqlc-dev/sqlc/internal/core" "github.com/sqlc-dev/sqlc/internal/sql/catalog" ) type Result struct { Catalog *catalog.Catalog Queries []*Query + + // CoreCatalog, when set, is the xqlc-derived catalog that drives both + // analysis and codegen for engines on the core (currently ClickHouse). + // When it is non-nil, codegen projects the catalog from it instead of + // from Catalog. + CoreCatalog *core.Catalog } diff --git a/internal/config/config.go b/internal/config/config.go index ff7faedcaa..6f0a9bf0eb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -54,6 +54,7 @@ const ( EngineMySQL Engine = "mysql" EnginePostgreSQL Engine = "postgresql" EngineSQLite Engine = "sqlite" + EngineClickHouse Engine = "clickhouse" ) type Config struct { diff --git a/internal/core/analyzer/scope.go b/internal/core/analyzer/scope.go index 3d688228a4..f7d78a1a23 100644 --- a/internal/core/analyzer/scope.go +++ b/internal/core/analyzer/scope.go @@ -24,8 +24,8 @@ type scopeRel struct { // scopeCol is a single column visible through a scopeRel. type scopeCol struct { - name string - attOID int64 + name string + attOID int64 typeOID int64 notNull bool } diff --git a/internal/core/attribute.go b/internal/core/attribute.go index 95e6cc7be4..540f87adb7 100644 --- a/internal/core/attribute.go +++ b/internal/core/attribute.go @@ -10,18 +10,18 @@ import ( // which is kept as a thin wrapper for callers that don't need to set // any of the "extra" metadata. type AttributeSpec struct { - ClassOID int64 - Name string - TypeOID int64 - Num int // 1-based ordinal position - NotNull bool - HasDefault bool - DeclType string // original declared type, before normalization - TypeLength int // varchar(N), numeric(p,s).p, etc. - TypeScale int // numeric(p,s).s - AutoIncrement bool - IsPrimaryKey bool - IsUnique bool + ClassOID int64 + Name string + TypeOID int64 + Num int // 1-based ordinal position + NotNull bool + HasDefault bool + DeclType string // original declared type, before normalization + TypeLength int // varchar(N), numeric(p,s).p, etc. + TypeScale int // numeric(p,s).s + AutoIncrement bool + IsPrimaryKey bool + IsUnique bool } // CreateAttributeSpec inserts a column with the full set of attribute @@ -94,18 +94,18 @@ func (c *Catalog) SetAttributeUnique(classOID int64, columns []string) error { // ColumnInfo describes a resolved column. type ColumnInfo struct { - Name string - TypeName string - TypeOID int64 - NotNull bool - DeclType string - TypeLength int - TypeScale int - AutoIncrement bool - IsPrimaryKey bool - IsUnique bool - AttributeOID int64 - ClassOID int64 + Name string + TypeName string + TypeOID int64 + NotNull bool + DeclType string + TypeLength int + TypeScale int + AutoIncrement bool + IsPrimaryKey bool + IsUnique bool + AttributeOID int64 + ClassOID int64 } // ResolveColumn looks up a column by table name and column name. @@ -174,17 +174,17 @@ func (c *Catalog) TableColumns(table string) ([]ColumnInfo, error) { // from sql_attribute joined back to sql_class / sql_namespace, so the // caller doesn't have to re-resolve names from raw OIDs. type AttributeDetails struct { - Schema string - Table string - Column string - Num int - DeclType string - TypeLength int - TypeScale int - AutoIncrement bool - IsPrimaryKey bool - IsUnique bool - NotNull bool + Schema string + Table string + Column string + Num int + DeclType string + TypeLength int + TypeScale int + AutoIncrement bool + IsPrimaryKey bool + IsUnique bool + NotNull bool } // LookupAttribute returns the full source-side metadata for a column, diff --git a/internal/core/cast_test.go b/internal/core/cast_test.go index 08c68b91c1..442ef4ebb6 100644 --- a/internal/core/cast_test.go +++ b/internal/core/cast_test.go @@ -31,14 +31,14 @@ func TestCastAllowed(t *testing.T) { ctx string want bool }{ - {intOID, intOID, "i", true}, // identity always OK - {intOID, bigintOID, "i", true}, // declared implicit - {intOID, bigintOID, "a", true}, // implicit subsumes assignment - {intOID, bigintOID, "e", true}, // implicit subsumes explicit - {intOID, textOID, "i", false}, // explicit-only blocked from implicit - {intOID, textOID, "a", false}, // and from assignment - {intOID, textOID, "e", true}, // but works for explicit - {textOID, intOID, "e", false}, // no rule defined + {intOID, intOID, "i", true}, // identity always OK + {intOID, bigintOID, "i", true}, // declared implicit + {intOID, bigintOID, "a", true}, // implicit subsumes assignment + {intOID, bigintOID, "e", true}, // implicit subsumes explicit + {intOID, textOID, "i", false}, // explicit-only blocked from implicit + {intOID, textOID, "a", false}, // and from assignment + {intOID, textOID, "e", true}, // but works for explicit + {textOID, intOID, "e", false}, // no rule defined } for _, c := range cases { got, err := cat.CastAllowed(c.src, c.tgt, c.ctx) diff --git a/internal/endtoend/testdata/clickhouse_select/clickhouse/db/db.go b/internal/endtoend/testdata/clickhouse_select/clickhouse/db/db.go new file mode 100644 index 0000000000..f43598b1eb --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/clickhouse/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/clickhouse_select/clickhouse/db/models.go b/internal/endtoend/testdata/clickhouse_select/clickhouse/db/models.go new file mode 100644 index 0000000000..21f493ecbc --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/clickhouse/db/models.go @@ -0,0 +1,5 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package db diff --git a/internal/endtoend/testdata/clickhouse_select/clickhouse/db/query.sql.go b/internal/endtoend/testdata/clickhouse_select/clickhouse/db/query.sql.go new file mode 100644 index 0000000000..19233a0b45 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/clickhouse/db/query.sql.go @@ -0,0 +1,52 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package db + +import ( + "context" + "time" +) + +const listEvents = `-- name: ListEvents :many +SELECT id, name, tag, amount, created FROM events; +` + +type ListEventsRow struct { + ID uint64 + Name string + Tag *string + Amount float64 + Created time.Time +} + +func (q *Queries) ListEvents(ctx context.Context) ([]ListEventsRow, error) { + rows, err := q.db.QueryContext(ctx, listEvents) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListEventsRow + for rows.Next() { + var i ListEventsRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Tag, + &i.Amount, + &i.Created, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/clickhouse_select/clickhouse/query.sql b/internal/endtoend/testdata/clickhouse_select/clickhouse/query.sql new file mode 100644 index 0000000000..4c756ec0f8 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/clickhouse/query.sql @@ -0,0 +1,2 @@ +-- name: ListEvents :many +SELECT id, name, tag, amount, created FROM events; diff --git a/internal/endtoend/testdata/clickhouse_select/clickhouse/schema.sql b/internal/endtoend/testdata/clickhouse_select/clickhouse/schema.sql new file mode 100644 index 0000000000..29960ee63d --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/clickhouse/schema.sql @@ -0,0 +1,7 @@ +CREATE TABLE events ( + id UInt64, + name String, + tag Nullable(String), + amount Float64, + created DateTime +) ENGINE = MergeTree ORDER BY id; diff --git a/internal/endtoend/testdata/clickhouse_select/clickhouse/sqlc.json b/internal/endtoend/testdata/clickhouse_select/clickhouse/sqlc.json new file mode 100644 index 0000000000..995102c9d6 --- /dev/null +++ b/internal/endtoend/testdata/clickhouse_select/clickhouse/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "clickhouse", + "queries": "query.sql", + "schema": "schema.sql", + "gen": { + "go": { + "package": "db", + "out": "db" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt b/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt index e2c49df3fa..28a5ce7e1f 100644 --- a/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt +++ b/internal/endtoend/testdata/parse_basic/clickhouse/stdout.txt @@ -1,5 +1,7 @@ [ { + "name": "GetValue", + "cmd": ":one", "ast": { "Stmt": { "DistinctClause": null, @@ -35,8 +37,8 @@ "Larg": null, "Rarg": null }, - "StmtLocation": 24, - "StmtLen": 0 + "StmtLocation": 0, + "StmtLen": 32 } } ] diff --git a/internal/engine/clickhouse/convert.go b/internal/engine/clickhouse/convert.go index f1fad62be7..aef7dd6419 100644 --- a/internal/engine/clickhouse/convert.go +++ b/internal/engine/clickhouse/convert.go @@ -486,8 +486,8 @@ func (c *cc) convertFunctionCall(n *chast.FunctionCall) *ast.FuncCall { Funcname: &ast.List{ Items: []ast.Node{&ast.String{Str: n.Name}}, }, - Location: n.Pos().Offset, - AggDistinct: n.Distinct, + Location: n.Pos().Offset, + AggDistinct: n.Distinct, } // Convert arguments diff --git a/internal/engine/clickhouse/parse.go b/internal/engine/clickhouse/parse.go index 282089f31d..83f9488dfe 100644 --- a/internal/engine/clickhouse/parse.go +++ b/internal/engine/clickhouse/parse.go @@ -29,36 +29,110 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { return nil, err } + // doubleclick exposes each statement's start (Pos, 1-based) but not its + // end (End == Pos). Reconstruct byte spans the way the other engines do: + // StmtLocation is a running offset that starts at the end of the + // previous statement, so leading comments (including the sqlc "-- name:" + // annotation) fall inside the statement's span, and StmtLen runs through + // the terminating semicolon. var stmts []ast.Statement + loc := 0 for _, stmt := range stmtNodes { + start := stmt.Pos().Offset - 1 // Pos is 1-based; convert to a byte index + if start < loc { + start = loc + } + end := statementEnd(blob, start) + converter := &cc{} out := converter.convert(stmt) if _, ok := out.(*ast.TODO); ok { + loc = end continue } - // Get position information from the statement - pos := stmt.Pos() - end := stmt.End() - stmtLen := end.Offset - pos.Offset - stmts = append(stmts, ast.Statement{ Raw: &ast.RawStmt{ Stmt: out, - StmtLocation: pos.Offset, - StmtLen: stmtLen, + StmtLocation: loc, + StmtLen: end - loc, }, }) + loc = end } return stmts, nil } +// statementEnd returns the byte index just past the terminating semicolon +// of the statement beginning at start, or len(blob) if there is none. It +// skips over string / quoted-identifier literals and comments so that a +// semicolon inside them does not end the statement early. +func statementEnd(blob []byte, start int) int { + for i := start; i < len(blob); i++ { + switch blob[i] { + case '\'', '"', '`': + i = skipQuoted(blob, i) + case '-': + if i+1 < len(blob) && blob[i+1] == '-' { + i = skipLineComment(blob, i) + } + case '#': + i = skipLineComment(blob, i) + case '/': + if i+1 < len(blob) && blob[i+1] == '*' { + i = skipBlockComment(blob, i) + } + case ';': + return i + 1 + } + } + return len(blob) +} + +// skipQuoted returns the index of the closing quote of the literal that +// opens at i (blob[i] is the opening quote). Backslash escapes and doubled +// quotes are honored. +func skipQuoted(blob []byte, i int) int { + q := blob[i] + for j := i + 1; j < len(blob); j++ { + switch blob[j] { + case '\\': + j++ // skip the escaped byte + case q: + if j+1 < len(blob) && blob[j+1] == q { + j++ // doubled quote is an escaped quote + continue + } + return j + } + } + return len(blob) - 1 +} + +func skipLineComment(blob []byte, i int) int { + for j := i; j < len(blob); j++ { + if blob[j] == '\n' { + return j + } + } + return len(blob) - 1 +} + +func skipBlockComment(blob []byte, i int) int { + for j := i + 2; j < len(blob); j++ { + if blob[j] == '*' && j+1 < len(blob) && blob[j+1] == '/' { + return j + 1 + } + } + return len(blob) - 1 +} + // https://clickhouse.com/docs/en/sql-reference/syntax#comments func (p *Parser) CommentSyntax() source.CommentSyntax { return source.CommentSyntax{ - Dash: true, // -- comment - SlashStar: true, // /* comment */ - Hash: true, // # comment (ClickHouse supports this) + Dash: true, // -- comment + SlashStar: true, // /* comment */ + Hash: true, // # comment (ClickHouse supports this) } } From 1241e5909fe196d48379c848f8f0f30fd3f2228b Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 27 Jul 2026 05:45:00 +0000 Subject: [PATCH 4/7] core: generate catalog accessors with sqlc (sqlc building sqlc) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce internal/core/catalogdb, a sqlc-generated SQLite query layer over the core catalog's own sql_* tables — sqlc analyzing sqlc's catalog. The catalog's schema.sql is the sqlc schema; catalogdb/query.sql holds the queries; //go:generate runs sqlc against them. Generation is offline and the output is committed, so there is no build-time cycle, and because the SQLite engine runs on the legacy compiler (not the core catalog) there is no analysis recursion. Vertical slice: types.go (CreateType/TypeOID/TypeName/LookupType) now delegates to the generated Queries. FindProcs is split into two generated queries — FindProcsAnyNamespace and FindProcsInNamespaces (sqlc.slice) — with Go choosing based on whether namespaces were supplied, replacing the hand-built IN-list. Remaining core files still use raw SQL and will be swept over incrementally. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XTGxNHW6v1S1YyC9FDSgrK --- internal/core/catalog.go | 16 +- internal/core/catalogdb/db.go | 31 ++++ internal/core/catalogdb/models.go | 111 ++++++++++++++ internal/core/catalogdb/query.sql | 37 +++++ internal/core/catalogdb/query.sql.go | 209 +++++++++++++++++++++++++++ internal/core/proc.go | 75 +++++----- internal/core/sqlc.json | 16 ++ internal/core/types.go | 81 ++++++----- 8 files changed, 504 insertions(+), 72 deletions(-) create mode 100644 internal/core/catalogdb/db.go create mode 100644 internal/core/catalogdb/models.go create mode 100644 internal/core/catalogdb/query.sql create mode 100644 internal/core/catalogdb/query.sql.go create mode 100644 internal/core/sqlc.json diff --git a/internal/core/catalog.go b/internal/core/catalog.go index ed3a015a72..4d526369d3 100644 --- a/internal/core/catalog.go +++ b/internal/core/catalog.go @@ -11,16 +11,30 @@ import ( _ "embed" "fmt" + "github.com/sqlc-dev/sqlc/internal/core/catalogdb" + _ "modernc.org/sqlite" ) +// The catalogdb package is generated by running sqlc against this +// package's own schema.sql and catalogdb/query.sql — sqlc analyzing +// sqlc's catalog. Regenerate with `go generate ./internal/core/...`. +// +//go:generate go run github.com/sqlc-dev/sqlc/cmd/sqlc generate + //go:embed schema.sql var ddl string // Catalog is an in-memory SQLite database that stores schema metadata in // sql_* tables modeled after PostgreSQL's system catalogs. +// +// Queries against the sql_* tables are increasingly served by the +// sqlc-generated catalogdb.Queries (see catalogdb/query.sql) rather than +// hand-written SQL — sqlc analyzing sqlc's own catalog. Methods not yet +// migrated fall back to the raw db handle. type Catalog struct { db *sql.DB + q *catalogdb.Queries } // Option configures a Catalog at creation time. The most common use is @@ -45,7 +59,7 @@ func New(opts ...Option) (*Catalog, error) { db.Close() return nil, fmt.Errorf("core: init schema: %w", err) } - c := &Catalog{db: db} + c := &Catalog{db: db, q: catalogdb.New(db)} if err := c.bootstrap(); err != nil { db.Close() return nil, fmt.Errorf("core: bootstrap: %w", err) diff --git a/internal/core/catalogdb/db.go b/internal/core/catalogdb/db.go new file mode 100644 index 0000000000..d16ad694e9 --- /dev/null +++ b/internal/core/catalogdb/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package catalogdb + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/core/catalogdb/models.go b/internal/core/catalogdb/models.go new file mode 100644 index 0000000000..374a2e70ee --- /dev/null +++ b/internal/core/catalogdb/models.go @@ -0,0 +1,111 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package catalogdb + +import ( + "database/sql" +) + +type SqlAttribute struct { + Oid int64 + ClassOid int64 + Name string + TypeOid int64 + NotNull int64 + HasDefault int64 + Num int64 + DeclType string + TypeLength int64 + TypeScale int64 + AutoIncrement int64 + IsPrimaryKey int64 + IsUnique int64 +} + +type SqlCast struct { + SourceTypeOid int64 + TargetTypeOid int64 + ProcOid sql.NullInt64 + Context string + DialectOid sql.NullInt64 +} + +type SqlClass struct { + Oid int64 + NamespaceOid int64 + Name string + Kind string +} + +type SqlConstraint struct { + Oid int64 + ClassOid int64 + Name string + Kind string + Columns string +} + +type SqlDialect struct { + Oid int64 + Name string +} + +type SqlDialectFlag struct { + DialectOid int64 + Key string + Value string +} + +type SqlNamespace struct { + Oid int64 + Name string +} + +type SqlOperator struct { + Oid int64 + NamespaceOid sql.NullInt64 + DialectOid sql.NullInt64 + Name string + LeftTypeOid sql.NullInt64 + RightTypeOid sql.NullInt64 + ResultTypeOid int64 + ProcOid sql.NullInt64 + CommutatorOid sql.NullInt64 + NegatorOid sql.NullInt64 +} + +type SqlProc struct { + Oid int64 + NamespaceOid sql.NullInt64 + DialectOid sql.NullInt64 + Name string + Kind string + ReturnTypeOid int64 + ReturnSet int64 + ReturnNullable int64 + Strict int64 + VariadicKind string +} + +type SqlProcArg struct { + ProcOid int64 + Ord int64 + Name string + TypeOid int64 + Mode string + HasDefault int64 +} + +type SqlType struct { + Oid int64 + NamespaceOid int64 + DialectOid sql.NullInt64 + Name string + Size int64 + Typtype string + Category sql.NullString + Preferred int64 + ElementOid sql.NullInt64 +} diff --git a/internal/core/catalogdb/query.sql b/internal/core/catalogdb/query.sql new file mode 100644 index 0000000000..01c2d79b68 --- /dev/null +++ b/internal/core/catalogdb/query.sql @@ -0,0 +1,37 @@ +-- name: CreateType :execlastid +INSERT INTO sql_type + (name, size, typtype, category, preferred, namespace_oid, dialect_oid, element_oid) +VALUES (?, ?, ?, ?, ?, ?, ?, ?); + +-- name: TypeOIDByName :one +SELECT t.oid +FROM sql_type t +JOIN sql_namespace ns ON ns.oid = t.namespace_oid +WHERE t.name = sqlc.arg(name) +ORDER BY + CASE ns.name + WHEN 'pg_catalog' THEN 0 + WHEN 'public' THEN 1 + ELSE 2 + END, + ns.name +LIMIT 1; + +-- name: TypeNameByOID :one +SELECT name FROM sql_type WHERE oid = ?; + +-- name: LookupType :one +SELECT oid, name, category, typtype, preferred +FROM sql_type +WHERE oid = ?; + +-- name: FindProcsAnyNamespace :many +SELECT oid, name, kind, return_type_oid, return_nullable +FROM sql_proc +WHERE name = ?; + +-- name: FindProcsInNamespaces :many +SELECT oid, name, kind, return_type_oid, return_nullable +FROM sql_proc +WHERE name = sqlc.arg(name) + AND namespace_oid IN (sqlc.slice(namespace_oids)); diff --git a/internal/core/catalogdb/query.sql.go b/internal/core/catalogdb/query.sql.go new file mode 100644 index 0000000000..b9a87200f2 --- /dev/null +++ b/internal/core/catalogdb/query.sql.go @@ -0,0 +1,209 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package catalogdb + +import ( + "context" + "database/sql" + "strings" +) + +const createType = `-- name: CreateType :execlastid +INSERT INTO sql_type + (name, size, typtype, category, preferred, namespace_oid, dialect_oid, element_oid) +VALUES (?, ?, ?, ?, ?, ?, ?, ?) +` + +type CreateTypeParams struct { + Name string + Size int64 + Typtype string + Category sql.NullString + Preferred int64 + NamespaceOid int64 + DialectOid sql.NullInt64 + ElementOid sql.NullInt64 +} + +func (q *Queries) CreateType(ctx context.Context, arg CreateTypeParams) (int64, error) { + result, err := q.db.ExecContext(ctx, createType, + arg.Name, + arg.Size, + arg.Typtype, + arg.Category, + arg.Preferred, + arg.NamespaceOid, + arg.DialectOid, + arg.ElementOid, + ) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +const findProcsAnyNamespace = `-- name: FindProcsAnyNamespace :many +SELECT oid, name, kind, return_type_oid, return_nullable +FROM sql_proc +WHERE name = ? +` + +type FindProcsAnyNamespaceRow struct { + Oid int64 + Name string + Kind string + ReturnTypeOid int64 + ReturnNullable int64 +} + +func (q *Queries) FindProcsAnyNamespace(ctx context.Context, name string) ([]FindProcsAnyNamespaceRow, error) { + rows, err := q.db.QueryContext(ctx, findProcsAnyNamespace, name) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindProcsAnyNamespaceRow + for rows.Next() { + var i FindProcsAnyNamespaceRow + if err := rows.Scan( + &i.Oid, + &i.Name, + &i.Kind, + &i.ReturnTypeOid, + &i.ReturnNullable, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const findProcsInNamespaces = `-- name: FindProcsInNamespaces :many +SELECT oid, name, kind, return_type_oid, return_nullable +FROM sql_proc +WHERE name = ?1 + AND namespace_oid IN (/*SLICE:namespace_oids*/?) +` + +type FindProcsInNamespacesParams struct { + Name string + NamespaceOids []sql.NullInt64 +} + +type FindProcsInNamespacesRow struct { + Oid int64 + Name string + Kind string + ReturnTypeOid int64 + ReturnNullable int64 +} + +func (q *Queries) FindProcsInNamespaces(ctx context.Context, arg FindProcsInNamespacesParams) ([]FindProcsInNamespacesRow, error) { + query := findProcsInNamespaces + var queryParams []interface{} + queryParams = append(queryParams, arg.Name) + if len(arg.NamespaceOids) > 0 { + for _, v := range arg.NamespaceOids { + queryParams = append(queryParams, v) + } + query = strings.Replace(query, "/*SLICE:namespace_oids*/?", strings.Repeat(",?", len(arg.NamespaceOids))[1:], 1) + } else { + query = strings.Replace(query, "/*SLICE:namespace_oids*/?", "NULL", 1) + } + rows, err := q.db.QueryContext(ctx, query, queryParams...) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindProcsInNamespacesRow + for rows.Next() { + var i FindProcsInNamespacesRow + if err := rows.Scan( + &i.Oid, + &i.Name, + &i.Kind, + &i.ReturnTypeOid, + &i.ReturnNullable, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const lookupType = `-- name: LookupType :one +SELECT oid, name, category, typtype, preferred +FROM sql_type +WHERE oid = ? +` + +type LookupTypeRow struct { + Oid int64 + Name string + Category sql.NullString + Typtype string + Preferred int64 +} + +func (q *Queries) LookupType(ctx context.Context, oid int64) (LookupTypeRow, error) { + row := q.db.QueryRowContext(ctx, lookupType, oid) + var i LookupTypeRow + err := row.Scan( + &i.Oid, + &i.Name, + &i.Category, + &i.Typtype, + &i.Preferred, + ) + return i, err +} + +const typeNameByOID = `-- name: TypeNameByOID :one +SELECT name FROM sql_type WHERE oid = ? +` + +func (q *Queries) TypeNameByOID(ctx context.Context, oid int64) (string, error) { + row := q.db.QueryRowContext(ctx, typeNameByOID, oid) + var name string + err := row.Scan(&name) + return name, err +} + +const typeOIDByName = `-- name: TypeOIDByName :one +SELECT t.oid +FROM sql_type t +JOIN sql_namespace ns ON ns.oid = t.namespace_oid +WHERE t.name = ?1 +ORDER BY + CASE ns.name + WHEN 'pg_catalog' THEN 0 + WHEN 'public' THEN 1 + ELSE 2 + END, + ns.name +LIMIT 1 +` + +func (q *Queries) TypeOIDByName(ctx context.Context, name string) (int64, error) { + row := q.db.QueryRowContext(ctx, typeOIDByName, name) + var oid int64 + err := row.Scan(&oid) + return oid, err +} diff --git a/internal/core/proc.go b/internal/core/proc.go index 181b5d755f..fd3fe1d7c6 100644 --- a/internal/core/proc.go +++ b/internal/core/proc.go @@ -1,9 +1,12 @@ package core import ( + "context" "database/sql" "fmt" "strings" + + "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) // ProcSpec describes a function/aggregate/window/procedure for insertion @@ -85,33 +88,47 @@ type ProcOverload struct { // any of the supplied namespaces. Pass an empty namespace list to search // every namespace. Argument lists are populated in declaration order. func (c *Catalog) FindProcs(name string, namespaceOIDs []int64) ([]ProcOverload, error) { - q := `SELECT oid, name, kind, return_type_oid, return_nullable - FROM sql_proc WHERE name = ?` - args := []any{strings.ToLower(name)} - if len(namespaceOIDs) > 0 { - q += " AND namespace_oid IN (" + placeholders(len(namespaceOIDs)) + ")" - for _, ns := range namespaceOIDs { - args = append(args, ns) - } - } - rows, err := c.db.Query(q, args...) - if err != nil { - return nil, fmt.Errorf("find procs %q: %w", name, err) - } - defer rows.Close() + ctx := context.Background() + lname := strings.ToLower(name) + var out []ProcOverload - for rows.Next() { - var o ProcOverload - var rn int - if err := rows.Scan(&o.OID, &o.Name, &o.Kind, &o.ReturnTypeOID, &rn); err != nil { - return nil, err + if len(namespaceOIDs) == 0 { + rows, err := c.q.FindProcsAnyNamespace(ctx, lname) + if err != nil { + return nil, fmt.Errorf("find procs %q: %w", name, err) + } + for _, r := range rows { + out = append(out, ProcOverload{ + OID: r.Oid, + Name: r.Name, + Kind: r.Kind, + ReturnTypeOID: r.ReturnTypeOid, + ReturnNullable: r.ReturnNullable != 0, + }) + } + } else { + nss := make([]sql.NullInt64, len(namespaceOIDs)) + for i, ns := range namespaceOIDs { + nss[i] = sql.NullInt64{Int64: ns, Valid: true} + } + rows, err := c.q.FindProcsInNamespaces(ctx, catalogdb.FindProcsInNamespacesParams{ + Name: lname, + NamespaceOids: nss, + }) + if err != nil { + return nil, fmt.Errorf("find procs %q: %w", name, err) + } + for _, r := range rows { + out = append(out, ProcOverload{ + OID: r.Oid, + Name: r.Name, + Kind: r.Kind, + ReturnTypeOID: r.ReturnTypeOid, + ReturnNullable: r.ReturnNullable != 0, + }) } - o.ReturnNullable = rn != 0 - out = append(out, o) - } - if err := rows.Err(); err != nil { - return nil, err } + for i := range out { argTypes, err := c.procArgTypes(out[i].OID) if err != nil { @@ -142,13 +159,3 @@ func (c *Catalog) procArgTypes(procOID int64) ([]int64, error) { } return out, rows.Err() } - -func placeholders(n int) string { - if n <= 0 { - return "" - } - return strings.Repeat("?,", n-1) + "?" -} - -// silence unused-import warning if we drop the sql.Null* usages -var _ sql.NullString diff --git a/internal/core/sqlc.json b/internal/core/sqlc.json new file mode 100644 index 0000000000..c77a8813ca --- /dev/null +++ b/internal/core/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "engine": "sqlite", + "schema": "schema.sql", + "queries": "catalogdb/query.sql", + "gen": { + "go": { + "package": "catalogdb", + "out": "catalogdb" + } + } + } + ] +} diff --git a/internal/core/types.go b/internal/core/types.go index 6acc2de1c0..069bfcd8ac 100644 --- a/internal/core/types.go +++ b/internal/core/types.go @@ -1,9 +1,12 @@ package core import ( + "context" "database/sql" "fmt" "strings" + + "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) // TypeSpec describes a SQL type for insertion into the catalog. @@ -39,18 +42,20 @@ func (c *Catalog) CreateTypeSpec(t TypeSpec) (int64, error) { } t.NamespaceOID = oid } - res, err := c.db.Exec( - `INSERT INTO sql_type - (name, size, typtype, category, preferred, namespace_oid, dialect_oid, element_oid) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - strings.ToLower(t.Name), t.Size, t.Typtype, - nullableString(t.Category), boolToInt(t.Preferred), - t.NamespaceOID, nullableOID(t.DialectOID), nullableOID(t.ElementOID), - ) + oid, err := c.q.CreateType(context.Background(), catalogdb.CreateTypeParams{ + Name: strings.ToLower(t.Name), + Size: int64(t.Size), + Typtype: t.Typtype, + Category: nullString(t.Category), + Preferred: boolToInt64(t.Preferred), + NamespaceOid: t.NamespaceOID, + DialectOid: nullInt64(t.DialectOID), + ElementOid: nullInt64(t.ElementOID), + }) if err != nil { return 0, fmt.Errorf("create type %q: %w", t.Name, err) } - return res.LastInsertId() + return oid, nil } // TypeOID returns the OID for the given (unqualified) type name. When @@ -58,19 +63,7 @@ func (c *Catalog) CreateTypeSpec(t TypeSpec) (int64, error) { // duplicate in information_schema and pg_catalog), the lookup prefers // pg_catalog, then public, then any other namespace by name. func (c *Catalog) TypeOID(name string) (int64, error) { - var oid int64 - err := c.db.QueryRow( - `SELECT t.oid FROM sql_type t - JOIN sql_namespace ns ON ns.oid = t.namespace_oid - WHERE t.name = ? - ORDER BY CASE ns.name - WHEN 'pg_catalog' THEN 0 - WHEN 'public' THEN 1 - ELSE 2 END, - ns.name - LIMIT 1`, - strings.ToLower(name), - ).Scan(&oid) + oid, err := c.q.TypeOIDByName(context.Background(), strings.ToLower(name)) if err != nil { return 0, fmt.Errorf("type %q: %w", name, err) } @@ -79,8 +72,7 @@ func (c *Catalog) TypeOID(name string) (int64, error) { // TypeName returns the name for the given type OID. func (c *Catalog) TypeName(oid int64) (string, error) { - var name string - err := c.db.QueryRow(`SELECT name FROM sql_type WHERE oid = ?`, oid).Scan(&name) + name, err := c.q.TypeNameByOID(context.Background(), oid) if err != nil { return "", fmt.Errorf("type oid %d: %w", oid, err) } @@ -98,23 +90,21 @@ type TypeInfo struct { // LookupType returns metadata for the given type OID. func (c *Catalog) LookupType(oid int64) (TypeInfo, error) { - var ( - ti TypeInfo - category sql.NullString - pref int - ) - err := c.db.QueryRow( - `SELECT oid, name, COALESCE(category, ''), typtype, preferred FROM sql_type WHERE oid = ?`, - oid, - ).Scan(&ti.OID, &ti.Name, &category, &ti.Typtype, &pref) + row, err := c.q.LookupType(context.Background(), oid) if err != nil { - return ti, fmt.Errorf("lookup type oid %d: %w", oid, err) + return TypeInfo{}, fmt.Errorf("lookup type oid %d: %w", oid, err) } - ti.Category = category.String - ti.Preferred = pref != 0 - return ti, nil + return TypeInfo{ + OID: row.Oid, + Name: row.Name, + Category: row.Category.String, + Typtype: row.Typtype, + Preferred: row.Preferred != 0, + }, nil } +// nullableOID / nullableString / boolToInt build the loosely-typed +// arguments the not-yet-migrated raw-SQL call sites pass to database/sql. func nullableOID(oid int64) any { if oid == 0 { return nil @@ -135,3 +125,20 @@ func boolToInt(b bool) int { } return 0 } + +// nullInt64 / nullString / boolToInt64 build the sql.Null* and int64 +// arguments the sqlc-generated catalogdb queries expect. +func nullInt64(oid int64) sql.NullInt64 { + return sql.NullInt64{Int64: oid, Valid: oid != 0} +} + +func nullString(s string) sql.NullString { + return sql.NullString{String: s, Valid: s != ""} +} + +func boolToInt64(b bool) int64 { + if b { + return 1 + } + return 0 +} From afea9bc3c19ea56af54a41050731db2e72d67ded Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 27 Jul 2026 05:56:51 +0000 Subject: [PATCH 5/7] core: migrate all catalog queries to sqlc-generated code Sweep the remaining hand-written SQL against the sql_* catalog tables onto the generated catalogdb layer, completing the conversion started in the types.go/FindProcs slice. Namespace, dialect, type, class, attribute, constraint, proc, operator, and cast queries now all run through catalogdb.Queries. Notable rewrites: - FindOperators: the conditionally-appended type filters become a single static query using (@arg = 0 OR col = @arg) sentinels. - Nullable OID columns round-trip through sql.NullInt64 (orZero maps NULL to the 0 sentinel the Go API uses). - New typed catalog accessors back the cross-package call sites that previously reached into the raw handle: DropClass (ClickHouse DROP TABLE), ClassColumns (analyzer scope), and Namespaces / TablesInNamespace / ClassCodegenColumns (codegen catalog projection). Only the schema bootstrap (db.Exec(ddl)) and the DB()/Close() handles remain as raw database/sql; every catalog query is now sqlc-generated. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XTGxNHW6v1S1YyC9FDSgrK --- internal/cmd/shim.go | 68 +-- internal/core/analyzer/scope.go | 33 +- internal/core/attribute.go | 224 ++++---- internal/core/cast.go | 44 +- internal/core/catalogdb/query.sql | 160 ++++++ internal/core/catalogdb/query.sql.go | 776 +++++++++++++++++++++++++++ internal/core/class.go | 63 ++- internal/core/constraint.go | 17 +- internal/core/dialect.go | 33 +- internal/core/namespace.go | 31 +- internal/core/operator.go | 66 ++- internal/core/proc.go | 58 +- internal/core/types.go | 9 + internal/engine/clickhouse/schema.go | 8 +- 14 files changed, 1296 insertions(+), 294 deletions(-) diff --git a/internal/cmd/shim.go b/internal/cmd/shim.go index 690364eee9..27e447de11 100644 --- a/internal/cmd/shim.go +++ b/internal/cmd/shim.go @@ -244,67 +244,41 @@ func codeGenRequest(r *compiler.Result, settings config.CombinedSettings) *plugi // pluginCatalogFromCore projects a core.Catalog (the xqlc SQLite-backed // catalog) into the plugin.Catalog that codegen consumes to emit models -// and enums. It reads the namespace / class / attribute / type tables -// directly. Projection is best-effort: an unexpected query error against -// the in-memory catalog yields a partial catalog rather than aborting -// generation. +// and enums. It reads through the catalog's typed accessors, which are +// backed by sqlc-generated queries. Projection is best-effort: an +// unexpected error against the in-memory catalog yields a partial catalog +// rather than aborting generation. func pluginCatalogFromCore(cc *core.Catalog) *plugin.Catalog { - db := cc.DB() var schemas []*plugin.Schema - type row struct { - oid int64 - name string + namespaces, err := cc.Namespaces() + if err != nil { + return &plugin.Catalog{DefaultSchema: "public"} } - readRows := func(query string, args ...any) []row { - rows, err := db.Query(query, args...) + for _, ns := range namespaces { + tables, err := cc.TablesInNamespace(ns.OID) if err != nil { - return nil - } - defer rows.Close() - var out []row - for rows.Next() { - var r row - if err := rows.Scan(&r.oid, &r.name); err != nil { - return out - } - out = append(out, r) + continue } - return out - } - - for _, ns := range readRows(`SELECT oid, name FROM sql_namespace ORDER BY oid`) { - var tables []*plugin.Table - for _, cl := range readRows( - `SELECT oid, name FROM sql_class WHERE namespace_oid = ? AND kind = 'r' ORDER BY oid`, ns.oid, - ) { - rel := &plugin.Identifier{Schema: ns.name, Name: cl.name} - var columns []*plugin.Column - crows, err := db.Query( - `SELECT a.name, t.name, a.not_null - FROM sql_attribute a JOIN sql_type t ON t.oid = a.type_oid - WHERE a.class_oid = ? ORDER BY a.num`, cl.oid, - ) + var ptables []*plugin.Table + for _, cl := range tables { + rel := &plugin.Identifier{Schema: ns.Name, Name: cl.Name} + cols, err := cc.ClassCodegenColumns(cl.OID) if err != nil { continue } - for crows.Next() { - var name, typeName string - var notNull int - if err := crows.Scan(&name, &typeName, ¬Null); err != nil { - break - } + var columns []*plugin.Column + for _, col := range cols { columns = append(columns, &plugin.Column{ - Name: name, - Type: &plugin.Identifier{Name: typeName}, - NotNull: notNull != 0, + Name: col.Name, + Type: &plugin.Identifier{Name: col.TypeName}, + NotNull: col.NotNull, Table: rel, }) } - crows.Close() - tables = append(tables, &plugin.Table{Rel: rel, Columns: columns}) + ptables = append(ptables, &plugin.Table{Rel: rel, Columns: columns}) } - schemas = append(schemas, &plugin.Schema{Name: ns.name, Tables: tables}) + schemas = append(schemas, &plugin.Schema{Name: ns.Name, Tables: ptables}) } return &plugin.Catalog{ diff --git a/internal/core/analyzer/scope.go b/internal/core/analyzer/scope.go index f7d78a1a23..1357a6b20f 100644 --- a/internal/core/analyzer/scope.go +++ b/internal/core/analyzer/scope.go @@ -102,30 +102,25 @@ func (a *analyzer) bindRangeVar(rv *ast.RangeVar) (scopeRel, error) { return rel, nil } -// classColumns reads the column layout of a relation directly from -// sql_attribute. We can't go through TableColumns because that resolves -// by name and doesn't return attribute OIDs. +// classColumns reads the column layout of a relation by class OID. It +// goes through the catalog's ClassColumns accessor (backed by a +// sqlc-generated query) rather than TableColumns, because it needs the +// attribute OIDs and resolves by OID rather than by name. func (a *analyzer) classColumns(classOID int64) ([]scopeCol, error) { - rows, err := a.cat.DB().Query( - `SELECT a.oid, a.name, a.type_oid, a.not_null - FROM sql_attribute a WHERE a.class_oid = ? ORDER BY a.num`, - classOID, - ) + cols, err := a.cat.ClassColumns(classOID) if err != nil { return nil, err } - defer rows.Close() - var out []scopeCol - for rows.Next() { - var c scopeCol - var nn int - if err := rows.Scan(&c.attOID, &c.name, &c.typeOID, &nn); err != nil { - return nil, err - } - c.notNull = nn != 0 - out = append(out, c) + out := make([]scopeCol, 0, len(cols)) + for _, c := range cols { + out = append(out, scopeCol{ + name: c.Name, + attOID: c.AttOID, + typeOID: c.TypeOID, + notNull: c.NotNull, + }) } - return out, rows.Err() + return out, nil } // resolveColumn locates a (relation, column) pair in the scope. diff --git a/internal/core/attribute.go b/internal/core/attribute.go index 540f87adb7..c546a53a64 100644 --- a/internal/core/attribute.go +++ b/internal/core/attribute.go @@ -1,8 +1,10 @@ package core import ( - "database/sql" + "context" "fmt" + + "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) // AttributeSpec carries the data needed to register a column on a @@ -27,19 +29,20 @@ type AttributeSpec struct { // CreateAttributeSpec inserts a column with the full set of attribute // metadata. func (c *Catalog) CreateAttributeSpec(s AttributeSpec) error { - _, err := c.db.Exec(` - INSERT INTO sql_attribute ( - class_oid, name, type_oid, not_null, has_default, num, - decl_type, type_length, type_scale, - auto_increment, is_primary_key, is_unique - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - s.ClassOID, s.Name, s.TypeOID, - boolToInt(s.NotNull), boolToInt(s.HasDefault), s.Num, - s.DeclType, s.TypeLength, s.TypeScale, - boolToInt(s.AutoIncrement), - boolToInt(s.IsPrimaryKey), - boolToInt(s.IsUnique), - ) + err := c.q.CreateAttribute(context.Background(), catalogdb.CreateAttributeParams{ + ClassOid: s.ClassOID, + Name: s.Name, + TypeOid: s.TypeOID, + NotNull: boolToInt64(s.NotNull), + HasDefault: boolToInt64(s.HasDefault), + Num: int64(s.Num), + DeclType: s.DeclType, + TypeLength: int64(s.TypeLength), + TypeScale: int64(s.TypeScale), + AutoIncrement: boolToInt64(s.AutoIncrement), + IsPrimaryKey: boolToInt64(s.IsPrimaryKey), + IsUnique: boolToInt64(s.IsUnique), + }) if err != nil { return fmt.Errorf("create attribute %q on class %d: %w", s.Name, s.ClassOID, err) } @@ -63,12 +66,12 @@ func (c *Catalog) CreateAttribute(classOID int64, name string, typeOID int64, no // on every named column of a relation. Used when a table-level PRIMARY // KEY constraint is processed after the columns have been registered. func (c *Catalog) SetAttributePrimaryKey(classOID int64, columns []string) error { + ctx := context.Background() for _, name := range columns { - _, err := c.db.Exec( - `UPDATE sql_attribute SET is_primary_key = 1, not_null = 1 - WHERE class_oid = ? AND name = ?`, - classOID, name, - ) + err := c.q.SetAttributePrimaryKey(ctx, catalogdb.SetAttributePrimaryKeyParams{ + ClassOid: classOID, + Name: name, + }) if err != nil { return fmt.Errorf("mark pk %s on class %d: %w", name, classOID, err) } @@ -80,11 +83,12 @@ func (c *Catalog) SetAttributePrimaryKey(classOID int64, columns []string) error // UNIQUE constraints don't make individual columns unique, so callers // should pass length-1 column lists only. func (c *Catalog) SetAttributeUnique(classOID int64, columns []string) error { + ctx := context.Background() for _, name := range columns { - _, err := c.db.Exec( - `UPDATE sql_attribute SET is_unique = 1 WHERE class_oid = ? AND name = ?`, - classOID, name, - ) + err := c.q.SetAttributeUnique(ctx, catalogdb.SetAttributeUniqueParams{ + ClassOid: classOID, + Name: name, + }) if err != nil { return fmt.Errorf("mark unique %s on class %d: %w", name, classOID, err) } @@ -110,64 +114,108 @@ type ColumnInfo struct { // ResolveColumn looks up a column by table name and column name. func (c *Catalog) ResolveColumn(table, column string) (*ColumnInfo, error) { - var info ColumnInfo - var ai, ipk, iu int - err := c.db.QueryRow(` - SELECT a.oid, a.class_oid, a.name, t.name, a.type_oid, a.not_null, - a.decl_type, a.type_length, a.type_scale, - a.auto_increment, a.is_primary_key, a.is_unique - FROM sql_attribute a - JOIN sql_class c ON c.oid = a.class_oid - JOIN sql_type t ON t.oid = a.type_oid - WHERE c.name = ? AND a.name = ? - `, table, column).Scan( - &info.AttributeOID, &info.ClassOID, &info.Name, &info.TypeName, &info.TypeOID, &info.NotNull, - &info.DeclType, &info.TypeLength, &info.TypeScale, - &ai, &ipk, &iu, - ) + r, err := c.q.ResolveColumn(context.Background(), catalogdb.ResolveColumnParams{ + TableName: table, + ColumnName: column, + }) if err != nil { return nil, fmt.Errorf("resolve %s.%s: %w", table, column, err) } - info.AutoIncrement = ai != 0 - info.IsPrimaryKey = ipk != 0 - info.IsUnique = iu != 0 + info := ColumnInfo{ + AttributeOID: r.Oid, + ClassOID: r.ClassOid, + Name: r.Name, + TypeName: r.TypeName, + TypeOID: r.TypeOid, + NotNull: r.NotNull != 0, + DeclType: r.DeclType, + TypeLength: int(r.TypeLength), + TypeScale: int(r.TypeScale), + AutoIncrement: r.AutoIncrement != 0, + IsPrimaryKey: r.IsPrimaryKey != 0, + IsUnique: r.IsUnique != 0, + } return &info, nil } // TableColumns returns all columns for a given table name, ordered by position. func (c *Catalog) TableColumns(table string) ([]ColumnInfo, error) { - rows, err := c.db.Query(` - SELECT a.oid, a.class_oid, a.name, t.name, a.type_oid, a.not_null, - a.decl_type, a.type_length, a.type_scale, - a.auto_increment, a.is_primary_key, a.is_unique - FROM sql_attribute a - JOIN sql_class c ON c.oid = a.class_oid - JOIN sql_type t ON t.oid = a.type_oid - WHERE c.name = ? - ORDER BY a.num - `, table) + rows, err := c.q.TableColumns(context.Background(), table) if err != nil { return nil, fmt.Errorf("table columns %q: %w", table, err) } - defer rows.Close() - - var cols []ColumnInfo - for rows.Next() { - var ci ColumnInfo - var ai, ipk, iu int - if err := rows.Scan( - &ci.AttributeOID, &ci.ClassOID, &ci.Name, &ci.TypeName, &ci.TypeOID, &ci.NotNull, - &ci.DeclType, &ci.TypeLength, &ci.TypeScale, - &ai, &ipk, &iu, - ); err != nil { - return nil, err - } - ci.AutoIncrement = ai != 0 - ci.IsPrimaryKey = ipk != 0 - ci.IsUnique = iu != 0 - cols = append(cols, ci) + cols := make([]ColumnInfo, 0, len(rows)) + for _, r := range rows { + cols = append(cols, ColumnInfo{ + AttributeOID: r.Oid, + ClassOID: r.ClassOid, + Name: r.Name, + TypeName: r.TypeName, + TypeOID: r.TypeOid, + NotNull: r.NotNull != 0, + DeclType: r.DeclType, + TypeLength: int(r.TypeLength), + TypeScale: int(r.TypeScale), + AutoIncrement: r.AutoIncrement != 0, + IsPrimaryKey: r.IsPrimaryKey != 0, + IsUnique: r.IsUnique != 0, + }) + } + return cols, nil +} + +// ClassColumn is a column of a relation identified by class OID, used by +// the analyzer to build its per-relation scope. +type ClassColumn struct { + AttOID int64 + Name string + TypeOID int64 + NotNull bool +} + +// ClassColumns returns the columns of a relation by class OID, ordered by +// position. +func (c *Catalog) ClassColumns(classOID int64) ([]ClassColumn, error) { + rows, err := c.q.ClassAttributes(context.Background(), classOID) + if err != nil { + return nil, fmt.Errorf("class columns %d: %w", classOID, err) + } + out := make([]ClassColumn, 0, len(rows)) + for _, r := range rows { + out = append(out, ClassColumn{ + AttOID: r.Oid, + Name: r.Name, + TypeOID: r.TypeOid, + NotNull: r.NotNull != 0, + }) + } + return out, nil +} + +// CodegenColumn is the minimal column shape codegen needs to emit a model +// field: the column name, its resolved type name, and nullability. +type CodegenColumn struct { + Name string + TypeName string + NotNull bool +} + +// ClassCodegenColumns returns the columns of a relation by class OID in the +// shape the codegen catalog projection consumes. +func (c *Catalog) ClassCodegenColumns(classOID int64) ([]CodegenColumn, error) { + rows, err := c.q.ListClassColumns(context.Background(), classOID) + if err != nil { + return nil, fmt.Errorf("class codegen columns %d: %w", classOID, err) + } + out := make([]CodegenColumn, 0, len(rows)) + for _, r := range rows { + out = append(out, CodegenColumn{ + Name: r.ColumnName, + TypeName: r.TypeName, + NotNull: r.NotNull != 0, + }) } - return cols, rows.Err() + return out, nil } // AttributeDetails describes a column resolved by its OID. Populated @@ -192,29 +240,21 @@ type AttributeDetails struct { // (and the per-column flag fields) once they've resolved an // expression to a sql_attribute row. func (c *Catalog) LookupAttribute(attOID int64) (AttributeDetails, error) { - var ad AttributeDetails - var schema sql.NullString - var ai, ipk, iu, nn int - err := c.db.QueryRow(` - SELECT ns.name, cls.name, a.name, a.num, - a.decl_type, a.type_length, a.type_scale, - a.auto_increment, a.is_primary_key, a.is_unique, a.not_null - FROM sql_attribute a - JOIN sql_class cls ON cls.oid = a.class_oid - JOIN sql_namespace ns ON ns.oid = cls.namespace_oid - WHERE a.oid = ? - `, attOID).Scan( - &schema, &ad.Table, &ad.Column, &ad.Num, - &ad.DeclType, &ad.TypeLength, &ad.TypeScale, - &ai, &ipk, &iu, &nn, - ) + r, err := c.q.LookupAttribute(context.Background(), attOID) if err != nil { - return ad, fmt.Errorf("lookup attribute %d: %w", attOID, err) + return AttributeDetails{}, fmt.Errorf("lookup attribute %d: %w", attOID, err) } - ad.Schema = schema.String - ad.AutoIncrement = ai != 0 - ad.IsPrimaryKey = ipk != 0 - ad.IsUnique = iu != 0 - ad.NotNull = nn != 0 - return ad, nil + return AttributeDetails{ + Schema: r.SchemaName, + Table: r.TableName, + Column: r.ColumnName, + Num: int(r.Num), + DeclType: r.DeclType, + TypeLength: int(r.TypeLength), + TypeScale: int(r.TypeScale), + AutoIncrement: r.AutoIncrement != 0, + IsPrimaryKey: r.IsPrimaryKey != 0, + IsUnique: r.IsUnique != 0, + NotNull: r.NotNull != 0, + }, nil } diff --git a/internal/core/cast.go b/internal/core/cast.go index 96f02f839c..f0464351e6 100644 --- a/internal/core/cast.go +++ b/internal/core/cast.go @@ -1,6 +1,11 @@ package core -import "fmt" +import ( + "context" + "fmt" + + "github.com/sqlc-dev/sqlc/internal/core/catalogdb" +) // CastSpec describes a type-coercion rule. // @@ -24,12 +29,13 @@ func (c *Catalog) CreateCast(cs CastSpec) error { if cs.Context == "" { cs.Context = "e" } - _, err := c.db.Exec( - `INSERT INTO sql_cast (source_type_oid, target_type_oid, proc_oid, context, dialect_oid) - VALUES (?, ?, ?, ?, ?)`, - cs.SourceTypeOID, cs.TargetTypeOID, - nullableOID(cs.ProcOID), cs.Context, nullableOID(cs.DialectOID), - ) + err := c.q.CreateCast(context.Background(), catalogdb.CreateCastParams{ + SourceTypeOid: cs.SourceTypeOID, + TargetTypeOid: cs.TargetTypeOID, + ProcOid: nullInt64(cs.ProcOID), + Context: cs.Context, + DialectOid: nullInt64(cs.DialectOID), + }) if err != nil { return fmt.Errorf("create cast %d->%d: %w", cs.SourceTypeOID, cs.TargetTypeOID, err) } @@ -39,20 +45,20 @@ func (c *Catalog) CreateCast(cs CastSpec) error { // FindCast returns the cast rule from src to tgt, if one exists, plus // whether it was found. func (c *Catalog) FindCast(src, tgt int64) (CastSpec, bool, error) { - var cs CastSpec - var procOID, dialectOID int64 - err := c.db.QueryRow( - `SELECT source_type_oid, target_type_oid, - COALESCE(proc_oid, 0), context, COALESCE(dialect_oid, 0) - FROM sql_cast WHERE source_type_oid = ? AND target_type_oid = ?`, - src, tgt, - ).Scan(&cs.SourceTypeOID, &cs.TargetTypeOID, &procOID, &cs.Context, &dialectOID) + row, err := c.q.FindCast(context.Background(), catalogdb.FindCastParams{ + SourceTypeOid: src, + TargetTypeOid: tgt, + }) if err != nil { - return cs, false, nil + return CastSpec{}, false, nil } - cs.ProcOID = procOID - cs.DialectOID = dialectOID - return cs, true, nil + return CastSpec{ + SourceTypeOID: row.SourceTypeOid, + TargetTypeOID: row.TargetTypeOid, + ProcOID: orZero(row.ProcOid), + Context: row.Context, + DialectOID: orZero(row.DialectOid), + }, true, nil } // CastAllowed reports whether src can be coerced to tgt in the given context. diff --git a/internal/core/catalogdb/query.sql b/internal/core/catalogdb/query.sql index 01c2d79b68..b1e487d370 100644 --- a/internal/core/catalogdb/query.sql +++ b/internal/core/catalogdb/query.sql @@ -1,3 +1,35 @@ +-- Queries against sqlc's own sql_* catalog tables, compiled by sqlc's +-- SQLite engine. Regenerate with `go generate ./internal/core/...`. + +-- ============================ sql_namespace ============================ + +-- name: CreateNamespace :execlastid +INSERT INTO sql_namespace (name) VALUES (?); + +-- name: NamespaceOID :one +SELECT oid FROM sql_namespace WHERE name = ?; + +-- name: ListNamespaces :many +SELECT oid, name FROM sql_namespace ORDER BY oid; + +-- ============================== sql_dialect ============================ + +-- name: CreateDialect :execlastid +INSERT INTO sql_dialect (name) VALUES (?); + +-- name: DialectOID :one +SELECT oid FROM sql_dialect WHERE name = ?; + +-- name: SetDialectFlag :exec +INSERT INTO sql_dialect_flag (dialect_oid, key, value) +VALUES (?, ?, ?) +ON CONFLICT(dialect_oid, key) DO UPDATE SET value = excluded.value; + +-- name: DialectFlag :one +SELECT value FROM sql_dialect_flag WHERE dialect_oid = ? AND key = ?; + +-- =============================== sql_type ============================== + -- name: CreateType :execlastid INSERT INTO sql_type (name, size, typtype, category, preferred, namespace_oid, dialect_oid, element_oid) @@ -25,6 +57,108 @@ SELECT oid, name, category, typtype, preferred FROM sql_type WHERE oid = ?; +-- =============================== sql_class ============================= + +-- name: CreateClass :execlastid +INSERT INTO sql_class (namespace_oid, name, kind) VALUES (?, ?, ?); + +-- name: ClassOID :one +SELECT oid FROM sql_class WHERE namespace_oid = ? AND name = ?; + +-- name: ClassOIDByName :one +SELECT oid FROM sql_class WHERE name = ? LIMIT 1; + +-- name: ListTablesInNamespace :many +SELECT oid, name FROM sql_class +WHERE namespace_oid = ? AND kind = 'r' +ORDER BY oid; + +-- name: DeleteClass :exec +DELETE FROM sql_class WHERE oid = ?; + +-- ============================= sql_attribute =========================== + +-- name: CreateAttribute :exec +INSERT INTO sql_attribute ( + class_oid, name, type_oid, not_null, has_default, num, + decl_type, type_length, type_scale, + auto_increment, is_primary_key, is_unique +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + +-- name: SetAttributePrimaryKey :exec +UPDATE sql_attribute SET is_primary_key = 1, not_null = 1 +WHERE class_oid = ? AND name = ?; + +-- name: SetAttributeUnique :exec +UPDATE sql_attribute SET is_unique = 1 +WHERE class_oid = ? AND name = ?; + +-- name: DeleteAttributesByClass :exec +DELETE FROM sql_attribute WHERE class_oid = ?; + +-- name: ResolveColumn :one +SELECT a.oid, a.class_oid, a.name, t.name AS type_name, a.type_oid, a.not_null, + a.decl_type, a.type_length, a.type_scale, + a.auto_increment, a.is_primary_key, a.is_unique +FROM sql_attribute a +JOIN sql_class c ON c.oid = a.class_oid +JOIN sql_type t ON t.oid = a.type_oid +WHERE c.name = sqlc.arg(table_name) AND a.name = sqlc.arg(column_name); + +-- name: TableColumns :many +SELECT a.oid, a.class_oid, a.name, t.name AS type_name, a.type_oid, a.not_null, + a.decl_type, a.type_length, a.type_scale, + a.auto_increment, a.is_primary_key, a.is_unique +FROM sql_attribute a +JOIN sql_class c ON c.oid = a.class_oid +JOIN sql_type t ON t.oid = a.type_oid +WHERE c.name = ? +ORDER BY a.num; + +-- name: ClassAttributes :many +SELECT oid, name, type_oid, not_null +FROM sql_attribute +WHERE class_oid = ? +ORDER BY num; + +-- name: ListClassColumns :many +SELECT a.name AS column_name, t.name AS type_name, a.not_null +FROM sql_attribute a +JOIN sql_type t ON t.oid = a.type_oid +WHERE a.class_oid = ? +ORDER BY a.num; + +-- name: LookupAttribute :one +SELECT ns.name AS schema_name, cls.name AS table_name, a.name AS column_name, a.num, + a.decl_type, a.type_length, a.type_scale, + a.auto_increment, a.is_primary_key, a.is_unique, a.not_null +FROM sql_attribute a +JOIN sql_class cls ON cls.oid = a.class_oid +JOIN sql_namespace ns ON ns.oid = cls.namespace_oid +WHERE a.oid = ?; + +-- ============================ sql_constraint =========================== + +-- name: CreateConstraint :exec +INSERT INTO sql_constraint (class_oid, name, kind, columns) VALUES (?, ?, ?, ?); + +-- =============================== sql_proc ============================== + +-- name: CreateProc :execlastid +INSERT INTO sql_proc + (namespace_oid, dialect_oid, name, kind, + return_type_oid, return_set, return_nullable, strict, variadic_kind) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + +-- name: CreateProcArg :exec +INSERT INTO sql_proc_arg (proc_oid, ord, name, type_oid, mode, has_default) +VALUES (?, ?, ?, ?, ?, ?); + +-- name: ProcArgTypes :many +SELECT type_oid FROM sql_proc_arg +WHERE proc_oid = ? AND mode IN ('i', 'b', 'v') +ORDER BY ord; + -- name: FindProcsAnyNamespace :many SELECT oid, name, kind, return_type_oid, return_nullable FROM sql_proc @@ -35,3 +169,29 @@ SELECT oid, name, kind, return_type_oid, return_nullable FROM sql_proc WHERE name = sqlc.arg(name) AND namespace_oid IN (sqlc.slice(namespace_oids)); + +-- ============================= sql_operator ============================ + +-- name: CreateOperator :execlastid +INSERT INTO sql_operator + (namespace_oid, dialect_oid, name, + left_type_oid, right_type_oid, result_type_oid, proc_oid) +VALUES (?, ?, ?, ?, ?, ?, ?); + +-- name: FindOperators :many +SELECT oid, name, left_type_oid, right_type_oid, result_type_oid, proc_oid +FROM sql_operator +WHERE name = sqlc.arg(name) + AND (sqlc.arg(left_type_oid) = 0 OR left_type_oid = sqlc.arg(left_type_oid)) + AND (sqlc.arg(right_type_oid) = 0 OR right_type_oid = sqlc.arg(right_type_oid)); + +-- =============================== sql_cast ============================== + +-- name: CreateCast :exec +INSERT INTO sql_cast (source_type_oid, target_type_oid, proc_oid, context, dialect_oid) +VALUES (?, ?, ?, ?, ?); + +-- name: FindCast :one +SELECT source_type_oid, target_type_oid, proc_oid, context, dialect_oid +FROM sql_cast +WHERE source_type_oid = ? AND target_type_oid = ?; diff --git a/internal/core/catalogdb/query.sql.go b/internal/core/catalogdb/query.sql.go index b9a87200f2..3ec3eacc73 100644 --- a/internal/core/catalogdb/query.sql.go +++ b/internal/core/catalogdb/query.sql.go @@ -11,7 +11,320 @@ import ( "strings" ) +const classAttributes = `-- name: ClassAttributes :many +SELECT oid, name, type_oid, not_null +FROM sql_attribute +WHERE class_oid = ? +ORDER BY num +` + +type ClassAttributesRow struct { + Oid int64 + Name string + TypeOid int64 + NotNull int64 +} + +func (q *Queries) ClassAttributes(ctx context.Context, classOid int64) ([]ClassAttributesRow, error) { + rows, err := q.db.QueryContext(ctx, classAttributes, classOid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ClassAttributesRow + for rows.Next() { + var i ClassAttributesRow + if err := rows.Scan( + &i.Oid, + &i.Name, + &i.TypeOid, + &i.NotNull, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const classOID = `-- name: ClassOID :one +SELECT oid FROM sql_class WHERE namespace_oid = ? AND name = ? +` + +type ClassOIDParams struct { + NamespaceOid int64 + Name string +} + +func (q *Queries) ClassOID(ctx context.Context, arg ClassOIDParams) (int64, error) { + row := q.db.QueryRowContext(ctx, classOID, arg.NamespaceOid, arg.Name) + var oid int64 + err := row.Scan(&oid) + return oid, err +} + +const classOIDByName = `-- name: ClassOIDByName :one +SELECT oid FROM sql_class WHERE name = ? LIMIT 1 +` + +func (q *Queries) ClassOIDByName(ctx context.Context, name string) (int64, error) { + row := q.db.QueryRowContext(ctx, classOIDByName, name) + var oid int64 + err := row.Scan(&oid) + return oid, err +} + +const createAttribute = `-- name: CreateAttribute :exec + +INSERT INTO sql_attribute ( + class_oid, name, type_oid, not_null, has_default, num, + decl_type, type_length, type_scale, + auto_increment, is_primary_key, is_unique +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +` + +type CreateAttributeParams struct { + ClassOid int64 + Name string + TypeOid int64 + NotNull int64 + HasDefault int64 + Num int64 + DeclType string + TypeLength int64 + TypeScale int64 + AutoIncrement int64 + IsPrimaryKey int64 + IsUnique int64 +} + +// ============================= sql_attribute =========================== +func (q *Queries) CreateAttribute(ctx context.Context, arg CreateAttributeParams) error { + _, err := q.db.ExecContext(ctx, createAttribute, + arg.ClassOid, + arg.Name, + arg.TypeOid, + arg.NotNull, + arg.HasDefault, + arg.Num, + arg.DeclType, + arg.TypeLength, + arg.TypeScale, + arg.AutoIncrement, + arg.IsPrimaryKey, + arg.IsUnique, + ) + return err +} + +const createCast = `-- name: CreateCast :exec + +INSERT INTO sql_cast (source_type_oid, target_type_oid, proc_oid, context, dialect_oid) +VALUES (?, ?, ?, ?, ?) +` + +type CreateCastParams struct { + SourceTypeOid int64 + TargetTypeOid int64 + ProcOid sql.NullInt64 + Context string + DialectOid sql.NullInt64 +} + +// =============================== sql_cast ============================== +func (q *Queries) CreateCast(ctx context.Context, arg CreateCastParams) error { + _, err := q.db.ExecContext(ctx, createCast, + arg.SourceTypeOid, + arg.TargetTypeOid, + arg.ProcOid, + arg.Context, + arg.DialectOid, + ) + return err +} + +const createClass = `-- name: CreateClass :execlastid + +INSERT INTO sql_class (namespace_oid, name, kind) VALUES (?, ?, ?) +` + +type CreateClassParams struct { + NamespaceOid int64 + Name string + Kind string +} + +// =============================== sql_class ============================= +func (q *Queries) CreateClass(ctx context.Context, arg CreateClassParams) (int64, error) { + result, err := q.db.ExecContext(ctx, createClass, arg.NamespaceOid, arg.Name, arg.Kind) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +const createConstraint = `-- name: CreateConstraint :exec + +INSERT INTO sql_constraint (class_oid, name, kind, columns) VALUES (?, ?, ?, ?) +` + +type CreateConstraintParams struct { + ClassOid int64 + Name string + Kind string + Columns string +} + +// ============================ sql_constraint =========================== +func (q *Queries) CreateConstraint(ctx context.Context, arg CreateConstraintParams) error { + _, err := q.db.ExecContext(ctx, createConstraint, + arg.ClassOid, + arg.Name, + arg.Kind, + arg.Columns, + ) + return err +} + +const createDialect = `-- name: CreateDialect :execlastid + +INSERT INTO sql_dialect (name) VALUES (?) +` + +// ============================== sql_dialect ============================ +func (q *Queries) CreateDialect(ctx context.Context, name string) (int64, error) { + result, err := q.db.ExecContext(ctx, createDialect, name) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +const createNamespace = `-- name: CreateNamespace :execlastid + + +INSERT INTO sql_namespace (name) VALUES (?) +` + +// Queries against sqlc's own sql_* catalog tables, compiled by sqlc's +// SQLite engine. Regenerate with `go generate ./internal/core/...`. +// ============================ sql_namespace ============================ +func (q *Queries) CreateNamespace(ctx context.Context, name string) (int64, error) { + result, err := q.db.ExecContext(ctx, createNamespace, name) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +const createOperator = `-- name: CreateOperator :execlastid + +INSERT INTO sql_operator + (namespace_oid, dialect_oid, name, + left_type_oid, right_type_oid, result_type_oid, proc_oid) +VALUES (?, ?, ?, ?, ?, ?, ?) +` + +type CreateOperatorParams struct { + NamespaceOid sql.NullInt64 + DialectOid sql.NullInt64 + Name string + LeftTypeOid sql.NullInt64 + RightTypeOid sql.NullInt64 + ResultTypeOid int64 + ProcOid sql.NullInt64 +} + +// ============================= sql_operator ============================ +func (q *Queries) CreateOperator(ctx context.Context, arg CreateOperatorParams) (int64, error) { + result, err := q.db.ExecContext(ctx, createOperator, + arg.NamespaceOid, + arg.DialectOid, + arg.Name, + arg.LeftTypeOid, + arg.RightTypeOid, + arg.ResultTypeOid, + arg.ProcOid, + ) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +const createProc = `-- name: CreateProc :execlastid + +INSERT INTO sql_proc + (namespace_oid, dialect_oid, name, kind, + return_type_oid, return_set, return_nullable, strict, variadic_kind) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) +` + +type CreateProcParams struct { + NamespaceOid sql.NullInt64 + DialectOid sql.NullInt64 + Name string + Kind string + ReturnTypeOid int64 + ReturnSet int64 + ReturnNullable int64 + Strict int64 + VariadicKind string +} + +// =============================== sql_proc ============================== +func (q *Queries) CreateProc(ctx context.Context, arg CreateProcParams) (int64, error) { + result, err := q.db.ExecContext(ctx, createProc, + arg.NamespaceOid, + arg.DialectOid, + arg.Name, + arg.Kind, + arg.ReturnTypeOid, + arg.ReturnSet, + arg.ReturnNullable, + arg.Strict, + arg.VariadicKind, + ) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +const createProcArg = `-- name: CreateProcArg :exec +INSERT INTO sql_proc_arg (proc_oid, ord, name, type_oid, mode, has_default) +VALUES (?, ?, ?, ?, ?, ?) +` + +type CreateProcArgParams struct { + ProcOid int64 + Ord int64 + Name string + TypeOid int64 + Mode string + HasDefault int64 +} + +func (q *Queries) CreateProcArg(ctx context.Context, arg CreateProcArgParams) error { + _, err := q.db.ExecContext(ctx, createProcArg, + arg.ProcOid, + arg.Ord, + arg.Name, + arg.TypeOid, + arg.Mode, + arg.HasDefault, + ) + return err +} + const createType = `-- name: CreateType :execlastid + INSERT INTO sql_type (name, size, typtype, category, preferred, namespace_oid, dialect_oid, element_oid) VALUES (?, ?, ?, ?, ?, ?, ?, ?) @@ -28,6 +341,7 @@ type CreateTypeParams struct { ElementOid sql.NullInt64 } +// =============================== sql_type ============================== func (q *Queries) CreateType(ctx context.Context, arg CreateTypeParams) (int64, error) { result, err := q.db.ExecContext(ctx, createType, arg.Name, @@ -45,6 +359,128 @@ func (q *Queries) CreateType(ctx context.Context, arg CreateTypeParams) (int64, return result.LastInsertId() } +const deleteAttributesByClass = `-- name: DeleteAttributesByClass :exec +DELETE FROM sql_attribute WHERE class_oid = ? +` + +func (q *Queries) DeleteAttributesByClass(ctx context.Context, classOid int64) error { + _, err := q.db.ExecContext(ctx, deleteAttributesByClass, classOid) + return err +} + +const deleteClass = `-- name: DeleteClass :exec +DELETE FROM sql_class WHERE oid = ? +` + +func (q *Queries) DeleteClass(ctx context.Context, oid int64) error { + _, err := q.db.ExecContext(ctx, deleteClass, oid) + return err +} + +const dialectFlag = `-- name: DialectFlag :one +SELECT value FROM sql_dialect_flag WHERE dialect_oid = ? AND key = ? +` + +type DialectFlagParams struct { + DialectOid int64 + Key string +} + +func (q *Queries) DialectFlag(ctx context.Context, arg DialectFlagParams) (string, error) { + row := q.db.QueryRowContext(ctx, dialectFlag, arg.DialectOid, arg.Key) + var value string + err := row.Scan(&value) + return value, err +} + +const dialectOID = `-- name: DialectOID :one +SELECT oid FROM sql_dialect WHERE name = ? +` + +func (q *Queries) DialectOID(ctx context.Context, name string) (int64, error) { + row := q.db.QueryRowContext(ctx, dialectOID, name) + var oid int64 + err := row.Scan(&oid) + return oid, err +} + +const findCast = `-- name: FindCast :one +SELECT source_type_oid, target_type_oid, proc_oid, context, dialect_oid +FROM sql_cast +WHERE source_type_oid = ? AND target_type_oid = ? +` + +type FindCastParams struct { + SourceTypeOid int64 + TargetTypeOid int64 +} + +func (q *Queries) FindCast(ctx context.Context, arg FindCastParams) (SqlCast, error) { + row := q.db.QueryRowContext(ctx, findCast, arg.SourceTypeOid, arg.TargetTypeOid) + var i SqlCast + err := row.Scan( + &i.SourceTypeOid, + &i.TargetTypeOid, + &i.ProcOid, + &i.Context, + &i.DialectOid, + ) + return i, err +} + +const findOperators = `-- name: FindOperators :many +SELECT oid, name, left_type_oid, right_type_oid, result_type_oid, proc_oid +FROM sql_operator +WHERE name = ?1 + AND (?2 = 0 OR left_type_oid = ?2) + AND (?3 = 0 OR right_type_oid = ?3) +` + +type FindOperatorsParams struct { + Name string + LeftTypeOid interface{} + RightTypeOid interface{} +} + +type FindOperatorsRow struct { + Oid int64 + Name string + LeftTypeOid sql.NullInt64 + RightTypeOid sql.NullInt64 + ResultTypeOid int64 + ProcOid sql.NullInt64 +} + +func (q *Queries) FindOperators(ctx context.Context, arg FindOperatorsParams) ([]FindOperatorsRow, error) { + rows, err := q.db.QueryContext(ctx, findOperators, arg.Name, arg.LeftTypeOid, arg.RightTypeOid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []FindOperatorsRow + for rows.Next() { + var i FindOperatorsRow + if err := rows.Scan( + &i.Oid, + &i.Name, + &i.LeftTypeOid, + &i.RightTypeOid, + &i.ResultTypeOid, + &i.ProcOid, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const findProcsAnyNamespace = `-- name: FindProcsAnyNamespace :many SELECT oid, name, kind, return_type_oid, return_nullable FROM sql_proc @@ -148,6 +584,147 @@ func (q *Queries) FindProcsInNamespaces(ctx context.Context, arg FindProcsInName return items, nil } +const listClassColumns = `-- name: ListClassColumns :many +SELECT a.name AS column_name, t.name AS type_name, a.not_null +FROM sql_attribute a +JOIN sql_type t ON t.oid = a.type_oid +WHERE a.class_oid = ? +ORDER BY a.num +` + +type ListClassColumnsRow struct { + ColumnName string + TypeName string + NotNull int64 +} + +func (q *Queries) ListClassColumns(ctx context.Context, classOid int64) ([]ListClassColumnsRow, error) { + rows, err := q.db.QueryContext(ctx, listClassColumns, classOid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListClassColumnsRow + for rows.Next() { + var i ListClassColumnsRow + if err := rows.Scan(&i.ColumnName, &i.TypeName, &i.NotNull); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listNamespaces = `-- name: ListNamespaces :many +SELECT oid, name FROM sql_namespace ORDER BY oid +` + +func (q *Queries) ListNamespaces(ctx context.Context) ([]SqlNamespace, error) { + rows, err := q.db.QueryContext(ctx, listNamespaces) + if err != nil { + return nil, err + } + defer rows.Close() + var items []SqlNamespace + for rows.Next() { + var i SqlNamespace + if err := rows.Scan(&i.Oid, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listTablesInNamespace = `-- name: ListTablesInNamespace :many +SELECT oid, name FROM sql_class +WHERE namespace_oid = ? AND kind = 'r' +ORDER BY oid +` + +type ListTablesInNamespaceRow struct { + Oid int64 + Name string +} + +func (q *Queries) ListTablesInNamespace(ctx context.Context, namespaceOid int64) ([]ListTablesInNamespaceRow, error) { + rows, err := q.db.QueryContext(ctx, listTablesInNamespace, namespaceOid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ListTablesInNamespaceRow + for rows.Next() { + var i ListTablesInNamespaceRow + if err := rows.Scan(&i.Oid, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const lookupAttribute = `-- name: LookupAttribute :one +SELECT ns.name AS schema_name, cls.name AS table_name, a.name AS column_name, a.num, + a.decl_type, a.type_length, a.type_scale, + a.auto_increment, a.is_primary_key, a.is_unique, a.not_null +FROM sql_attribute a +JOIN sql_class cls ON cls.oid = a.class_oid +JOIN sql_namespace ns ON ns.oid = cls.namespace_oid +WHERE a.oid = ? +` + +type LookupAttributeRow struct { + SchemaName string + TableName string + ColumnName string + Num int64 + DeclType string + TypeLength int64 + TypeScale int64 + AutoIncrement int64 + IsPrimaryKey int64 + IsUnique int64 + NotNull int64 +} + +func (q *Queries) LookupAttribute(ctx context.Context, oid int64) (LookupAttributeRow, error) { + row := q.db.QueryRowContext(ctx, lookupAttribute, oid) + var i LookupAttributeRow + err := row.Scan( + &i.SchemaName, + &i.TableName, + &i.ColumnName, + &i.Num, + &i.DeclType, + &i.TypeLength, + &i.TypeScale, + &i.AutoIncrement, + &i.IsPrimaryKey, + &i.IsUnique, + &i.NotNull, + ) + return i, err +} + const lookupType = `-- name: LookupType :one SELECT oid, name, category, typtype, preferred FROM sql_type @@ -175,6 +752,205 @@ func (q *Queries) LookupType(ctx context.Context, oid int64) (LookupTypeRow, err return i, err } +const namespaceOID = `-- name: NamespaceOID :one +SELECT oid FROM sql_namespace WHERE name = ? +` + +func (q *Queries) NamespaceOID(ctx context.Context, name string) (int64, error) { + row := q.db.QueryRowContext(ctx, namespaceOID, name) + var oid int64 + err := row.Scan(&oid) + return oid, err +} + +const procArgTypes = `-- name: ProcArgTypes :many +SELECT type_oid FROM sql_proc_arg +WHERE proc_oid = ? AND mode IN ('i', 'b', 'v') +ORDER BY ord +` + +func (q *Queries) ProcArgTypes(ctx context.Context, procOid int64) ([]int64, error) { + rows, err := q.db.QueryContext(ctx, procArgTypes, procOid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var type_oid int64 + if err := rows.Scan(&type_oid); err != nil { + return nil, err + } + items = append(items, type_oid) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const resolveColumn = `-- name: ResolveColumn :one +SELECT a.oid, a.class_oid, a.name, t.name AS type_name, a.type_oid, a.not_null, + a.decl_type, a.type_length, a.type_scale, + a.auto_increment, a.is_primary_key, a.is_unique +FROM sql_attribute a +JOIN sql_class c ON c.oid = a.class_oid +JOIN sql_type t ON t.oid = a.type_oid +WHERE c.name = ?1 AND a.name = ?2 +` + +type ResolveColumnParams struct { + TableName string + ColumnName string +} + +type ResolveColumnRow struct { + Oid int64 + ClassOid int64 + Name string + TypeName string + TypeOid int64 + NotNull int64 + DeclType string + TypeLength int64 + TypeScale int64 + AutoIncrement int64 + IsPrimaryKey int64 + IsUnique int64 +} + +func (q *Queries) ResolveColumn(ctx context.Context, arg ResolveColumnParams) (ResolveColumnRow, error) { + row := q.db.QueryRowContext(ctx, resolveColumn, arg.TableName, arg.ColumnName) + var i ResolveColumnRow + err := row.Scan( + &i.Oid, + &i.ClassOid, + &i.Name, + &i.TypeName, + &i.TypeOid, + &i.NotNull, + &i.DeclType, + &i.TypeLength, + &i.TypeScale, + &i.AutoIncrement, + &i.IsPrimaryKey, + &i.IsUnique, + ) + return i, err +} + +const setAttributePrimaryKey = `-- name: SetAttributePrimaryKey :exec +UPDATE sql_attribute SET is_primary_key = 1, not_null = 1 +WHERE class_oid = ? AND name = ? +` + +type SetAttributePrimaryKeyParams struct { + ClassOid int64 + Name string +} + +func (q *Queries) SetAttributePrimaryKey(ctx context.Context, arg SetAttributePrimaryKeyParams) error { + _, err := q.db.ExecContext(ctx, setAttributePrimaryKey, arg.ClassOid, arg.Name) + return err +} + +const setAttributeUnique = `-- name: SetAttributeUnique :exec +UPDATE sql_attribute SET is_unique = 1 +WHERE class_oid = ? AND name = ? +` + +type SetAttributeUniqueParams struct { + ClassOid int64 + Name string +} + +func (q *Queries) SetAttributeUnique(ctx context.Context, arg SetAttributeUniqueParams) error { + _, err := q.db.ExecContext(ctx, setAttributeUnique, arg.ClassOid, arg.Name) + return err +} + +const setDialectFlag = `-- name: SetDialectFlag :exec +INSERT INTO sql_dialect_flag (dialect_oid, key, value) +VALUES (?, ?, ?) +ON CONFLICT(dialect_oid, key) DO UPDATE SET value = excluded.value +` + +type SetDialectFlagParams struct { + DialectOid int64 + Key string + Value string +} + +func (q *Queries) SetDialectFlag(ctx context.Context, arg SetDialectFlagParams) error { + _, err := q.db.ExecContext(ctx, setDialectFlag, arg.DialectOid, arg.Key, arg.Value) + return err +} + +const tableColumns = `-- name: TableColumns :many +SELECT a.oid, a.class_oid, a.name, t.name AS type_name, a.type_oid, a.not_null, + a.decl_type, a.type_length, a.type_scale, + a.auto_increment, a.is_primary_key, a.is_unique +FROM sql_attribute a +JOIN sql_class c ON c.oid = a.class_oid +JOIN sql_type t ON t.oid = a.type_oid +WHERE c.name = ? +ORDER BY a.num +` + +type TableColumnsRow struct { + Oid int64 + ClassOid int64 + Name string + TypeName string + TypeOid int64 + NotNull int64 + DeclType string + TypeLength int64 + TypeScale int64 + AutoIncrement int64 + IsPrimaryKey int64 + IsUnique int64 +} + +func (q *Queries) TableColumns(ctx context.Context, name string) ([]TableColumnsRow, error) { + rows, err := q.db.QueryContext(ctx, tableColumns, name) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TableColumnsRow + for rows.Next() { + var i TableColumnsRow + if err := rows.Scan( + &i.Oid, + &i.ClassOid, + &i.Name, + &i.TypeName, + &i.TypeOid, + &i.NotNull, + &i.DeclType, + &i.TypeLength, + &i.TypeScale, + &i.AutoIncrement, + &i.IsPrimaryKey, + &i.IsUnique, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const typeNameByOID = `-- name: TypeNameByOID :one SELECT name FROM sql_type WHERE oid = ? ` diff --git a/internal/core/class.go b/internal/core/class.go index 5aad1b21f0..4de691cbfd 100644 --- a/internal/core/class.go +++ b/internal/core/class.go @@ -1,27 +1,32 @@ package core -import "fmt" +import ( + "context" + "fmt" + + "github.com/sqlc-dev/sqlc/internal/core/catalogdb" +) // CreateClass inserts a relation (table, view, etc.) and returns its OID. // Kind should be 'r' (table), 'v' (view), or 'i' (index). func (c *Catalog) CreateClass(namespaceOID int64, name string, kind string) (int64, error) { - res, err := c.db.Exec( - `INSERT INTO sql_class (namespace_oid, name, kind) VALUES (?, ?, ?)`, - namespaceOID, name, kind, - ) + oid, err := c.q.CreateClass(context.Background(), catalogdb.CreateClassParams{ + NamespaceOid: namespaceOID, + Name: name, + Kind: kind, + }) if err != nil { return 0, fmt.Errorf("create class %q: %w", name, err) } - return res.LastInsertId() + return oid, nil } // ClassOID returns the OID for the given relation in the given namespace. func (c *Catalog) ClassOID(namespaceOID int64, name string) (int64, error) { - var oid int64 - err := c.db.QueryRow( - `SELECT oid FROM sql_class WHERE namespace_oid = ? AND name = ?`, - namespaceOID, name, - ).Scan(&oid) + oid, err := c.q.ClassOID(context.Background(), catalogdb.ClassOIDParams{ + NamespaceOid: namespaceOID, + Name: name, + }) if err != nil { return 0, fmt.Errorf("class %q: %w", name, err) } @@ -31,12 +36,40 @@ func (c *Catalog) ClassOID(namespaceOID int64, name string) (int64, error) { // ClassOIDByName looks up a relation by name across all namespaces. // If multiple matches exist, it returns the first found. func (c *Catalog) ClassOIDByName(name string) (int64, error) { - var oid int64 - err := c.db.QueryRow( - `SELECT oid FROM sql_class WHERE name = ? LIMIT 1`, name, - ).Scan(&oid) + oid, err := c.q.ClassOIDByName(context.Background(), name) if err != nil { return 0, fmt.Errorf("class %q: %w", name, err) } return oid, nil } + +// DropClass removes a relation and all of its columns from the catalog. +func (c *Catalog) DropClass(classOID int64) error { + ctx := context.Background() + if err := c.q.DeleteAttributesByClass(ctx, classOID); err != nil { + return fmt.Errorf("drop class %d attributes: %w", classOID, err) + } + if err := c.q.DeleteClass(ctx, classOID); err != nil { + return fmt.Errorf("drop class %d: %w", classOID, err) + } + return nil +} + +// ClassInfo is a relation row exposed for codegen catalog projection. +type ClassInfo struct { + OID int64 + Name string +} + +// TablesInNamespace returns the tables (kind 'r') in a namespace, by OID. +func (c *Catalog) TablesInNamespace(namespaceOID int64) ([]ClassInfo, error) { + rows, err := c.q.ListTablesInNamespace(context.Background(), namespaceOID) + if err != nil { + return nil, fmt.Errorf("list tables in namespace %d: %w", namespaceOID, err) + } + out := make([]ClassInfo, 0, len(rows)) + for _, r := range rows { + out = append(out, ClassInfo{OID: r.Oid, Name: r.Name}) + } + return out, nil +} diff --git a/internal/core/constraint.go b/internal/core/constraint.go index 7f87479623..08a055d849 100644 --- a/internal/core/constraint.go +++ b/internal/core/constraint.go @@ -1,15 +1,22 @@ package core -import "fmt" +import ( + "context" + "fmt" + + "github.com/sqlc-dev/sqlc/internal/core/catalogdb" +) // CreateConstraint inserts a constraint for the given relation. // Kind should be 'p' (primary key), 'f' (foreign key), 'u' (unique), or 'c' (check). // columns is a comma-separated list of attribute ordinal positions. func (c *Catalog) CreateConstraint(classOID int64, name string, kind string, columns string) error { - _, err := c.db.Exec( - `INSERT INTO sql_constraint (class_oid, name, kind, columns) VALUES (?, ?, ?, ?)`, - classOID, name, kind, columns, - ) + err := c.q.CreateConstraint(context.Background(), catalogdb.CreateConstraintParams{ + ClassOid: classOID, + Name: name, + Kind: kind, + Columns: columns, + }) if err != nil { return fmt.Errorf("create constraint %q on class %d: %w", name, classOID, err) } diff --git a/internal/core/dialect.go b/internal/core/dialect.go index 283f63f57f..850d0f5ace 100644 --- a/internal/core/dialect.go +++ b/internal/core/dialect.go @@ -1,20 +1,24 @@ package core -import "fmt" +import ( + "context" + "fmt" + + "github.com/sqlc-dev/sqlc/internal/core/catalogdb" +) // CreateDialect inserts a SQL dialect and returns its OID. func (c *Catalog) CreateDialect(name string) (int64, error) { - res, err := c.db.Exec(`INSERT INTO sql_dialect (name) VALUES (?)`, name) + oid, err := c.q.CreateDialect(context.Background(), name) if err != nil { return 0, fmt.Errorf("create dialect %q: %w", name, err) } - return res.LastInsertId() + return oid, nil } // DialectOID returns the OID for a registered dialect. func (c *Catalog) DialectOID(name string) (int64, error) { - var oid int64 - err := c.db.QueryRow(`SELECT oid FROM sql_dialect WHERE name = ?`, name).Scan(&oid) + oid, err := c.q.DialectOID(context.Background(), name) if err != nil { return 0, fmt.Errorf("dialect %q: %w", name, err) } @@ -24,11 +28,11 @@ func (c *Catalog) DialectOID(name string) (int64, error) { // SetDialectFlag stores a per-dialect configuration value. // If the key already exists, the value is replaced. func (c *Catalog) SetDialectFlag(dialectOID int64, key, value string) error { - _, err := c.db.Exec( - `INSERT INTO sql_dialect_flag (dialect_oid, key, value) VALUES (?, ?, ?) - ON CONFLICT(dialect_oid, key) DO UPDATE SET value = excluded.value`, - dialectOID, key, value, - ) + err := c.q.SetDialectFlag(context.Background(), catalogdb.SetDialectFlagParams{ + DialectOid: dialectOID, + Key: key, + Value: value, + }) if err != nil { return fmt.Errorf("set dialect flag %s.%s: %w", key, value, err) } @@ -37,11 +41,10 @@ func (c *Catalog) SetDialectFlag(dialectOID int64, key, value string) error { // DialectFlag returns the value of a dialect flag, or "" if not set. func (c *Catalog) DialectFlag(dialectOID int64, key string) (string, error) { - var value string - err := c.db.QueryRow( - `SELECT value FROM sql_dialect_flag WHERE dialect_oid = ? AND key = ?`, - dialectOID, key, - ).Scan(&value) + value, err := c.q.DialectFlag(context.Background(), catalogdb.DialectFlagParams{ + DialectOid: dialectOID, + Key: key, + }) if err != nil { return "", nil // missing flag = empty string, not an error } diff --git a/internal/core/namespace.go b/internal/core/namespace.go index 498f89d795..1f83f536a3 100644 --- a/internal/core/namespace.go +++ b/internal/core/namespace.go @@ -1,22 +1,43 @@ package core -import "fmt" +import ( + "context" + "fmt" +) // CreateNamespace inserts a namespace and returns its OID. func (c *Catalog) CreateNamespace(name string) (int64, error) { - res, err := c.db.Exec(`INSERT INTO sql_namespace (name) VALUES (?)`, name) + oid, err := c.q.CreateNamespace(context.Background(), name) if err != nil { return 0, fmt.Errorf("create namespace %q: %w", name, err) } - return res.LastInsertId() + return oid, nil } // NamespaceOID returns the OID for the given namespace name. func (c *Catalog) NamespaceOID(name string) (int64, error) { - var oid int64 - err := c.db.QueryRow(`SELECT oid FROM sql_namespace WHERE name = ?`, name).Scan(&oid) + oid, err := c.q.NamespaceOID(context.Background(), name) if err != nil { return 0, fmt.Errorf("namespace %q: %w", name, err) } return oid, nil } + +// NamespaceInfo is a namespace row exposed for codegen catalog projection. +type NamespaceInfo struct { + OID int64 + Name string +} + +// Namespaces returns every namespace, ordered by OID. +func (c *Catalog) Namespaces() ([]NamespaceInfo, error) { + rows, err := c.q.ListNamespaces(context.Background()) + if err != nil { + return nil, fmt.Errorf("list namespaces: %w", err) + } + out := make([]NamespaceInfo, 0, len(rows)) + for _, r := range rows { + out = append(out, NamespaceInfo{OID: r.Oid, Name: r.Name}) + } + return out, nil +} diff --git a/internal/core/operator.go b/internal/core/operator.go index 2e77739901..4cf2125d94 100644 --- a/internal/core/operator.go +++ b/internal/core/operator.go @@ -1,6 +1,11 @@ package core -import "fmt" +import ( + "context" + "fmt" + + "github.com/sqlc-dev/sqlc/internal/core/catalogdb" +) // OperatorSpec describes an operator overload for insertion into the catalog. // A NULL left_type_oid encodes a prefix unary operator; NULL right_type_oid @@ -17,19 +22,19 @@ type OperatorSpec struct { // CreateOperator inserts an operator overload and returns its OID. func (c *Catalog) CreateOperator(o OperatorSpec) (int64, error) { - res, err := c.db.Exec( - `INSERT INTO sql_operator - (namespace_oid, dialect_oid, name, - left_type_oid, right_type_oid, result_type_oid, proc_oid) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - nullableOID(o.NamespaceOID), nullableOID(o.DialectOID), o.Name, - nullableOID(o.LeftTypeOID), nullableOID(o.RightTypeOID), - o.ResultTypeOID, nullableOID(o.ProcOID), - ) + oid, err := c.q.CreateOperator(context.Background(), catalogdb.CreateOperatorParams{ + NamespaceOid: nullInt64(o.NamespaceOID), + DialectOid: nullInt64(o.DialectOID), + Name: o.Name, + LeftTypeOid: nullInt64(o.LeftTypeOID), + RightTypeOid: nullInt64(o.RightTypeOID), + ResultTypeOid: o.ResultTypeOID, + ProcOid: nullInt64(o.ProcOID), + }) if err != nil { return 0, fmt.Errorf("create operator %q: %w", o.Name, err) } - return res.LastInsertId() + return oid, nil } // OperatorOverload is a resolved candidate from FindOperators. @@ -46,31 +51,24 @@ type OperatorOverload struct { // (left,right) operand types. A 0 type means "any" and skips the filter on // that side; useful for listing all overloads of a name. func (c *Catalog) FindOperators(name string, leftTypeOID, rightTypeOID int64) ([]OperatorOverload, error) { - q := `SELECT oid, name, - COALESCE(left_type_oid, 0), COALESCE(right_type_oid, 0), - result_type_oid, COALESCE(proc_oid, 0) - FROM sql_operator WHERE name = ?` - args := []any{name} - if leftTypeOID != 0 { - q += ` AND left_type_oid = ?` - args = append(args, leftTypeOID) - } - if rightTypeOID != 0 { - q += ` AND right_type_oid = ?` - args = append(args, rightTypeOID) - } - rows, err := c.db.Query(q, args...) + rows, err := c.q.FindOperators(context.Background(), catalogdb.FindOperatorsParams{ + Name: name, + LeftTypeOid: leftTypeOID, + RightTypeOid: rightTypeOID, + }) if err != nil { return nil, fmt.Errorf("find operators %q: %w", name, err) } - defer rows.Close() - var out []OperatorOverload - for rows.Next() { - var o OperatorOverload - if err := rows.Scan(&o.OID, &o.Name, &o.LeftTypeOID, &o.RightTypeOID, &o.ResultTypeOID, &o.ProcOID); err != nil { - return nil, err - } - out = append(out, o) + out := make([]OperatorOverload, 0, len(rows)) + for _, r := range rows { + out = append(out, OperatorOverload{ + OID: r.Oid, + Name: r.Name, + LeftTypeOID: orZero(r.LeftTypeOid), + RightTypeOID: orZero(r.RightTypeOid), + ResultTypeOID: r.ResultTypeOid, + ProcOID: orZero(r.ProcOid), + }) } - return out, rows.Err() + return out, nil } diff --git a/internal/core/proc.go b/internal/core/proc.go index fd3fe1d7c6..58539ac9ba 100644 --- a/internal/core/proc.go +++ b/internal/core/proc.go @@ -40,33 +40,34 @@ func (c *Catalog) CreateProc(p ProcSpec) (int64, error) { if p.VariadicKind == "" { p.VariadicKind = "n" } - res, err := c.db.Exec( - `INSERT INTO sql_proc - (namespace_oid, dialect_oid, name, kind, - return_type_oid, return_set, return_nullable, strict, variadic_kind) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - nullableOID(p.NamespaceOID), nullableOID(p.DialectOID), - strings.ToLower(p.Name), p.Kind, - p.ReturnTypeOID, boolToInt(p.ReturnSet), boolToInt(p.ReturnNullable), - boolToInt(p.Strict), p.VariadicKind, - ) + ctx := context.Background() + procOID, err := c.q.CreateProc(ctx, catalogdb.CreateProcParams{ + NamespaceOid: nullInt64(p.NamespaceOID), + DialectOid: nullInt64(p.DialectOID), + Name: strings.ToLower(p.Name), + Kind: p.Kind, + ReturnTypeOid: p.ReturnTypeOID, + ReturnSet: boolToInt64(p.ReturnSet), + ReturnNullable: boolToInt64(p.ReturnNullable), + Strict: boolToInt64(p.Strict), + VariadicKind: p.VariadicKind, + }) if err != nil { return 0, fmt.Errorf("create proc %q: %w", p.Name, err) } - procOID, err := res.LastInsertId() - if err != nil { - return 0, err - } for i, a := range p.Args { mode := a.Mode if mode == "" { mode = "i" } - _, err := c.db.Exec( - `INSERT INTO sql_proc_arg (proc_oid, ord, name, type_oid, mode, has_default) - VALUES (?, ?, ?, ?, ?, ?)`, - procOID, i+1, a.Name, a.TypeOID, mode, boolToInt(a.HasDefault), - ) + err := c.q.CreateProcArg(ctx, catalogdb.CreateProcArgParams{ + ProcOid: procOID, + Ord: int64(i + 1), + Name: a.Name, + TypeOid: a.TypeOID, + Mode: mode, + HasDefault: boolToInt64(a.HasDefault), + }) if err != nil { return 0, fmt.Errorf("create proc %q arg %d: %w", p.Name, i+1, err) } @@ -140,22 +141,5 @@ func (c *Catalog) FindProcs(name string, namespaceOIDs []int64) ([]ProcOverload, } func (c *Catalog) procArgTypes(procOID int64) ([]int64, error) { - rows, err := c.db.Query( - `SELECT type_oid FROM sql_proc_arg - WHERE proc_oid = ? AND mode IN ('i','b','v') - ORDER BY ord`, procOID, - ) - if err != nil { - return nil, err - } - defer rows.Close() - var out []int64 - for rows.Next() { - var t int64 - if err := rows.Scan(&t); err != nil { - return nil, err - } - out = append(out, t) - } - return out, rows.Err() + return c.q.ProcArgTypes(context.Background(), procOID) } diff --git a/internal/core/types.go b/internal/core/types.go index 069bfcd8ac..55a50dd0e5 100644 --- a/internal/core/types.go +++ b/internal/core/types.go @@ -142,3 +142,12 @@ func boolToInt64(b bool) int64 { } return 0 } + +// orZero unwraps a nullable OID column, mapping NULL to the 0 sentinel +// the catalog's Go API uses for "unset". +func orZero(n sql.NullInt64) int64 { + if n.Valid { + return n.Int64 + } + return 0 +} diff --git a/internal/engine/clickhouse/schema.go b/internal/engine/clickhouse/schema.go index 283d71e70a..a2ffad38be 100644 --- a/internal/engine/clickhouse/schema.go +++ b/internal/engine/clickhouse/schema.go @@ -113,12 +113,8 @@ func applyDropTable(cat *core.Catalog, stmt *ast.DropTableStmt) error { } return fmt.Errorf("clickhouse: drop table %q: %w", tn.Name, err) } - db := cat.DB() - if _, err := db.Exec(`DELETE FROM sql_attribute WHERE class_oid = ?`, classOID); err != nil { - return err - } - if _, err := db.Exec(`DELETE FROM sql_class WHERE oid = ?`, classOID); err != nil { - return err + if err := cat.DropClass(classOID); err != nil { + return fmt.Errorf("clickhouse: drop table %q: %w", tn.Name, err) } } return nil From 27bc2d893f8ffde8d62fbc94c70a7d05624ebde2 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 27 Jul 2026 14:23:53 +0000 Subject: [PATCH 6/7] core: move catalog SQL into a catalogdef package, export Schema Extract schema.sql and query.sql from internal/core (and catalogdb) into a dedicated internal/core/catalogdef package that owns the SQL defining sqlc's catalog and embeds the schema (catalogdef.Schema). sqlc reads its schema and queries from catalogdef and still generates the querier into catalogdb, keeping SQL sources separate from generated code. core/catalog.go now builds its in-memory catalog from catalogdef.Schema instead of embedding schema.sql itself, so the DDL used at runtime and the schema fed to codegen are one source of truth. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XTGxNHW6v1S1YyC9FDSgrK --- internal/core/catalog.go | 20 ++++++++----------- internal/core/catalogdef/embed.go | 14 +++++++++++++ .../core/{catalogdb => catalogdef}/query.sql | 0 internal/core/{ => catalogdef}/schema.sql | 0 internal/core/sqlc.json | 4 ++-- 5 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 internal/core/catalogdef/embed.go rename internal/core/{catalogdb => catalogdef}/query.sql (100%) rename internal/core/{ => catalogdef}/schema.sql (100%) diff --git a/internal/core/catalog.go b/internal/core/catalog.go index 4d526369d3..73f3d8587a 100644 --- a/internal/core/catalog.go +++ b/internal/core/catalog.go @@ -8,30 +8,26 @@ package core import ( "database/sql" - _ "embed" "fmt" "github.com/sqlc-dev/sqlc/internal/core/catalogdb" + "github.com/sqlc-dev/sqlc/internal/core/catalogdef" _ "modernc.org/sqlite" ) -// The catalogdb package is generated by running sqlc against this -// package's own schema.sql and catalogdb/query.sql — sqlc analyzing -// sqlc's catalog. Regenerate with `go generate ./internal/core/...`. +// The catalogdb package is generated by running sqlc against the SQL in +// catalogdef (schema.sql + query.sql) — sqlc analyzing sqlc's catalog. +// Regenerate with `go generate ./internal/core/...`. // //go:generate go run github.com/sqlc-dev/sqlc/cmd/sqlc generate -//go:embed schema.sql -var ddl string - // Catalog is an in-memory SQLite database that stores schema metadata in // sql_* tables modeled after PostgreSQL's system catalogs. // -// Queries against the sql_* tables are increasingly served by the -// sqlc-generated catalogdb.Queries (see catalogdb/query.sql) rather than -// hand-written SQL — sqlc analyzing sqlc's own catalog. Methods not yet -// migrated fall back to the raw db handle. +// Queries against the sql_* tables are served by the sqlc-generated +// catalogdb.Queries (compiled from catalogdef/query.sql) — sqlc analyzing +// sqlc's own catalog. type Catalog struct { db *sql.DB q *catalogdb.Queries @@ -55,7 +51,7 @@ func New(opts ...Option) (*Catalog, error) { if err != nil { return nil, fmt.Errorf("core: open catalog: %w", err) } - if _, err := db.Exec(ddl); err != nil { + if _, err := db.Exec(catalogdef.Schema); err != nil { db.Close() return nil, fmt.Errorf("core: init schema: %w", err) } diff --git a/internal/core/catalogdef/embed.go b/internal/core/catalogdef/embed.go new file mode 100644 index 0000000000..c7de3b8efc --- /dev/null +++ b/internal/core/catalogdef/embed.go @@ -0,0 +1,14 @@ +// Package catalogdef holds the SQL that defines sqlc's internal catalog: +// schema.sql (the sql_* table definitions) and query.sql (the queries +// against them). Both are the input to sqlc's own code generation — sqlc +// analyzing sqlc's catalog — and schema.sql is embedded here so the core +// package builds its in-memory catalog from the same source of truth that +// codegen reads. +package catalogdef + +import _ "embed" + +// Schema is the DDL that creates the sql_* catalog tables. +// +//go:embed schema.sql +var Schema string diff --git a/internal/core/catalogdb/query.sql b/internal/core/catalogdef/query.sql similarity index 100% rename from internal/core/catalogdb/query.sql rename to internal/core/catalogdef/query.sql diff --git a/internal/core/schema.sql b/internal/core/catalogdef/schema.sql similarity index 100% rename from internal/core/schema.sql rename to internal/core/catalogdef/schema.sql diff --git a/internal/core/sqlc.json b/internal/core/sqlc.json index c77a8813ca..3b4763b697 100644 --- a/internal/core/sqlc.json +++ b/internal/core/sqlc.json @@ -3,8 +3,8 @@ "sql": [ { "engine": "sqlite", - "schema": "schema.sql", - "queries": "catalogdb/query.sql", + "schema": "catalogdef/schema.sql", + "queries": "catalogdef/query.sql", "gen": { "go": { "package": "catalogdb", From 3e6640c89d76eb948342cb8f88918049ce09a86e Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 27 Jul 2026 14:34:14 +0000 Subject: [PATCH 7/7] clickhouse/core: strip comments and remove xqlc references Remove comments from the Go files authored for this work (catalog core, analyzer, ClickHouse engine, compile path, codegen type map) and drop the comments added to the files touched along the way, keeping //go:generate and //go:embed directives. No remaining references to xqlc. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XTGxNHW6v1S1YyC9FDSgrK --- internal/cmd/shim.go | 9 ----- internal/codegen/golang/clickhouse_type.go | 9 ----- internal/compiler/compile.go | 2 - internal/compiler/engine.go | 6 --- internal/compiler/parse.go | 2 - internal/compiler/parse_clickhouse.go | 12 ------ internal/compiler/result.go | 4 -- internal/core/analysis.go | 33 ----------------- internal/core/analyzer/analyzer.go | 16 -------- internal/core/analyzer/analyzer_test.go | 7 ---- internal/core/analyzer/expr.go | 43 +--------------------- internal/core/analyzer/projection.go | 10 ----- internal/core/analyzer/scope.go | 23 +----------- internal/core/attribute.go | 40 ++------------------ internal/core/cast.go | 16 +------- internal/core/cast_test.go | 18 ++++----- internal/core/catalog.go | 29 --------------- internal/core/catalogdef/embed.go | 8 ---- internal/core/class.go | 8 ---- internal/core/constraint.go | 3 -- internal/core/dialect.go | 7 +--- internal/core/dialect_test.go | 2 - internal/core/namespace.go | 4 -- internal/core/operator.go | 24 ++++-------- internal/core/operator_test.go | 2 - internal/core/proc.go | 18 +++------ internal/core/proc_test.go | 2 - internal/core/types.go | 30 +++------------ internal/engine/clickhouse/analyze_test.go | 6 --- internal/engine/clickhouse/convert.go | 12 ------ internal/engine/clickhouse/parse.go | 19 ++-------- internal/engine/clickhouse/schema.go | 29 --------------- internal/engine/clickhouse/seed.go | 30 +-------------- 33 files changed, 39 insertions(+), 444 deletions(-) diff --git a/internal/cmd/shim.go b/internal/cmd/shim.go index 27e447de11..5361849ca6 100644 --- a/internal/cmd/shim.go +++ b/internal/cmd/shim.go @@ -225,9 +225,6 @@ func pluginQueryParam(p compiler.Parameter) *plugin.Parameter { } func codeGenRequest(r *compiler.Result, settings config.CombinedSettings) *plugin.GenerateRequest { - // Engines on the xqlc core (ClickHouse) project the codegen catalog - // from the core catalog rather than the in-memory sql/catalog, which is - // nil on that path. var cat *plugin.Catalog if r.CoreCatalog != nil { cat = pluginCatalogFromCore(r.CoreCatalog) @@ -242,12 +239,6 @@ func codeGenRequest(r *compiler.Result, settings config.CombinedSettings) *plugi } } -// pluginCatalogFromCore projects a core.Catalog (the xqlc SQLite-backed -// catalog) into the plugin.Catalog that codegen consumes to emit models -// and enums. It reads through the catalog's typed accessors, which are -// backed by sqlc-generated queries. Projection is best-effort: an -// unexpected error against the in-memory catalog yields a partial catalog -// rather than aborting generation. func pluginCatalogFromCore(cc *core.Catalog) *plugin.Catalog { var schemas []*plugin.Schema diff --git a/internal/codegen/golang/clickhouse_type.go b/internal/codegen/golang/clickhouse_type.go index 93613eec06..f1550b3534 100644 --- a/internal/codegen/golang/clickhouse_type.go +++ b/internal/codegen/golang/clickhouse_type.go @@ -8,10 +8,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/plugin" ) -// clickhouseType maps a ClickHouse column type to a Go type. Type names -// arrive lower-cased from the core catalog (e.g. "uint64", "string", -// "datetime"). Nullable columns (NotNull == false) map to a pointer, which -// is how the clickhouse-go driver represents Nullable(T). func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string { dt := strings.ToLower(sdk.DataType(col.Type)) notNull := col.NotNull @@ -34,7 +30,6 @@ func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plu case "int64": return nullable(notNull, "int64") case "uint128", "uint256", "int128", "int256": - // Big integers are represented as *big.Int by clickhouse-go. return "*big.Int" case "float32", "bfloat16": return nullable(notNull, "float32") @@ -47,9 +42,6 @@ func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plu case "date", "date32", "datetime", "datetime64": return nullable(notNull, "time.Time") - // The following resolve to string for now; richer mappings - // (decimal.Decimal, uuid.UUID, netip.Addr, json.RawMessage) require - // wiring their imports into the Go importer and are a follow-up. case "decimal", "decimal32", "decimal64", "decimal128", "decimal256", "uuid", "ipv4", "ipv6", "json", "enum8", "enum16": return nullable(notNull, "string") @@ -59,7 +51,6 @@ func clickhouseType(req *plugin.GenerateRequest, options *opts.Options, col *plu } } -// nullable wraps a base Go type in a pointer when the column is nullable. func nullable(notNull bool, base string) string { if notNull { return base diff --git a/internal/compiler/compile.go b/internal/compiler/compile.go index b5665c28ef..534521c7a2 100644 --- a/internal/compiler/compile.go +++ b/internal/compiler/compile.go @@ -56,8 +56,6 @@ func (c *Compiler) parseCatalog(schemas []string) error { continue } - // ClickHouse populates the core catalog instead of the in-memory - // sql/catalog. if c.coreCatalog != nil { for i := range stmts { if err := clickhouse.Apply(c.coreCatalog, stmts[i].Raw); err != nil { diff --git a/internal/compiler/engine.go b/internal/compiler/engine.go index 43767b0faf..8b50d30426 100644 --- a/internal/compiler/engine.go +++ b/internal/compiler/engine.go @@ -29,9 +29,6 @@ type Compiler struct { client dbmanager.Client selector selector - // coreCatalog is the xqlc-derived catalog used by engines whose - // analysis runs on the core analyzer (currently ClickHouse) instead of - // the legacy compiler analyze step. It is nil for other engines. coreCatalog *core.Catalog schema []string @@ -119,9 +116,6 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts } } case config.EngineClickHouse: - // ClickHouse runs on the xqlc analysis core: its schema and queries - // are resolved against a core.Catalog by the core analyzer, not the - // legacy compiler analyze step or the in-memory sql/catalog. c.parser = clickhouse.NewParser() c.selector = newDefaultSelector() cat, err := core.New(clickhouse.Dialect()) diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go index 61b79ec1f7..be510d71d4 100644 --- a/internal/compiler/parse.go +++ b/internal/compiler/parse.go @@ -19,8 +19,6 @@ import ( var debugDumpAST = sqlcdebug.New("dumpast") func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, error) { - // ClickHouse resolves types through the core analyzer, entirely - // bypassing the legacy analyze step below. if c.coreCatalog != nil { return c.parseQueryCore(stmt, src) } diff --git a/internal/compiler/parse_clickhouse.go b/internal/compiler/parse_clickhouse.go index f10466b764..9d79b21709 100644 --- a/internal/compiler/parse_clickhouse.go +++ b/internal/compiler/parse_clickhouse.go @@ -12,12 +12,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/validate" ) -// parseQueryCore is the analysis path for engines backed by the xqlc core -// catalog and analyzer (currently ClickHouse). It reuses the shared, -// engine-agnostic query-metadata parsing but resolves columns and -// parameters through core/analyzer, never touching the legacy compiler -// analyze step (inferQuery/outputColumns/parameters) or the -// analyzer.Analyzer seam. func (c *Compiler) parseQueryCore(stmt ast.Node, src string) (*Query, error) { raw, ok := stmt.(*ast.RawStmt) if !ok { @@ -54,9 +48,6 @@ func (c *Compiler) parseQueryCore(stmt ast.Node, src string) (*Query, error) { var cols []*Column var params []Parameter - // Only result-shaped statements produce columns/parameters. Non-SELECT - // statements (:exec, DDL) currently yield an empty shape; growing the - // core analyzer to cover INSERT/UPDATE/DELETE is a follow-up. if _, ok := raw.Stmt.(*ast.SelectStmt); ok { res, err := coreanalyzer.Prepare(c.coreCatalog, raw) if err != nil { @@ -91,9 +82,6 @@ func (c *Compiler) parseQueryCore(stmt ast.Node, src string) (*Query, error) { }, nil } -// coreColumn maps a core.Column (analyzer output) onto the compiler's -// Column. The type name is carried in DataType; Type is left nil so the -// codegen shim uses DataType directly. func coreColumn(c core.Column) *Column { col := &Column{ Name: c.Name, diff --git a/internal/compiler/result.go b/internal/compiler/result.go index e77933d555..69d78d8923 100644 --- a/internal/compiler/result.go +++ b/internal/compiler/result.go @@ -9,9 +9,5 @@ type Result struct { Catalog *catalog.Catalog Queries []*Query - // CoreCatalog, when set, is the xqlc-derived catalog that drives both - // analysis and codegen for engines on the core (currently ClickHouse). - // When it is non-nil, codegen projects the catalog from it instead of - // from Catalog. CoreCatalog *core.Catalog } diff --git a/internal/core/analysis.go b/internal/core/analysis.go index 2cb61cbf53..b437c46609 100644 --- a/internal/core/analysis.go +++ b/internal/core/analysis.go @@ -1,8 +1,5 @@ package core -// Command identifies the kind of statement that produced a PrepareResult. -// Only the four DML statements that can have a prepare-able shape are -// emitted; DDL and TCL produce an empty result with Command == "". type Command string const ( @@ -12,22 +9,12 @@ const ( CommandDelete Command = "DELETE" ) -// PrepareResult describes the output of preparing a SQL statement. type PrepareResult struct { Command Command `json:"command,omitempty"` Columns []Column `json:"columns"` Parameters []Parameter `json:"parameters"` } -// ColumnSource identifies the table column a result column or bind -// parameter is sourced from. All fields are optional; the struct is -// emitted only when at least one is populated. -// -// Schema / Table / Column are the *origin* identifiers (pre-alias) — -// they correspond to sqlite's `sqlite3_column_origin_name` and mysql's -// `org_table` / `org_name`. TableAlias is the name the query used to -// refer to the table; it lets codegen distinguish `t1` from `t2` in -// `SELECT t1.x, t2.x FROM t t1 JOIN t t2 ...`. type ColumnSource struct { Schema string `json:"schema,omitempty"` Table string `json:"table,omitempty"` @@ -35,17 +22,6 @@ type ColumnSource struct { Column string `json:"column,omitempty"` } -// Column describes a single output column from a prepared statement. -// -// SourceClassOID and SourceAttributeOID are populated when the column -// is a direct reference to a table column (i.e. it appears in -// sql_attribute); they are zero for computed/derived expressions like -// aggregates or arithmetic. Source is the human-readable resolution of -// those OIDs and is populated under the same conditions. -// -// DeclType, TypeLength, TypeScale, IsPrimaryKey, IsUnique, and -// IsAutoIncrement come from the resolved source attribute and are zero -// for computed expressions. type Column struct { Name string `json:"name"` DataType string `json:"data_type"` @@ -62,15 +38,6 @@ type Column struct { IsAutoIncrement bool `json:"is_auto_increment,omitempty"` } -// Parameter describes a bind parameter in a prepared statement. -// -// Number is the 1-based position of the parameter as it appeared in the -// source ($1, $2, ...). Name is populated for named-parameter dialects -// or when a sqlc-style "-- name:" annotation gave the param a name; it -// is empty otherwise. DataType / TypeOID / NotNull come from the -// resolved usage site (an operator overload, function argument, etc.). -// Source identifies the column the parameter binds against (e.g. -// `users.age` for `WHERE age > $1`), when one can be inferred. type Parameter struct { Number int `json:"number"` Name string `json:"name,omitempty"` diff --git a/internal/core/analyzer/analyzer.go b/internal/core/analyzer/analyzer.go index 39da95585a..d4fcc57bbc 100644 --- a/internal/core/analyzer/analyzer.go +++ b/internal/core/analyzer/analyzer.go @@ -1,10 +1,3 @@ -// Package analyzer implements a dialect-neutral SQL query analyzer that -// resolves names, types, operators, and parameters by querying the -// catalog (core.Catalog). It produces a core.PrepareResult. -// -// Scope is intentionally narrow in this iteration: single-relation SELECT -// queries, simple WHERE / GROUP BY / projection. JOINs, subqueries, -// CTEs, set ops, and DML RETURNING are intended follow-ups. package analyzer import ( @@ -14,10 +7,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/ast" ) -// Prepare walks a parsed statement and produces a PrepareResult by -// querying the catalog for relations, types, operators, and casts. -// stmt can be a *ast.RawStmt (typical parser output) or an unwrapped -// statement node. func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) { if rs, ok := stmt.(*ast.RawStmt); ok { stmt = rs.Stmt @@ -80,8 +69,6 @@ func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error { } a.scope = sc - // Join ON conditions get typed against the (already-assembled) - // scope so they can reference columns from either side. for _, item := range listItems(s.FromClause) { if err := a.typeJoinConditions(item); err != nil { return fmt.Errorf("join: %w", err) @@ -129,9 +116,6 @@ func listItems(l *ast.List) []ast.Node { return l.Items } -// typeJoinConditions walks a FROM-list item and types every join's ON -// expression. USING clauses are skipped — the columns they reference -// already exist in scope, no expression to type. func (a *analyzer) typeJoinConditions(item ast.Node) error { je, ok := item.(*ast.JoinExpr) if !ok { diff --git a/internal/core/analyzer/analyzer_test.go b/internal/core/analyzer/analyzer_test.go index b7300921b7..22204e9af9 100644 --- a/internal/core/analyzer/analyzer_test.go +++ b/internal/core/analyzer/analyzer_test.go @@ -9,10 +9,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/engine/postgresql" ) -// seedUsers builds a minimal catalog with a single "users" table so the -// analyzer has something to resolve against. It deliberately avoids any -// per-dialect seed: the point is to exercise the dialect-neutral -// analyzer directly on sqlc's ast. func seedUsers(t *testing.T) *core.Catalog { t.Helper() cat, err := core.New() @@ -46,9 +42,6 @@ func seedUsers(t *testing.T) *core.Catalog { return cat } -// prepare parses query with sqlc's own PostgreSQL parser — which emits -// exactly the internal/sql/ast the analyzer was repointed onto — and -// runs the analyzer against cat. func prepare(t *testing.T, cat *core.Catalog, query string) core.PrepareResult { t.Helper() stmts, err := postgresql.NewParser().Parse(strings.NewReader(query)) diff --git a/internal/core/analyzer/expr.go b/internal/core/analyzer/expr.go index fdf9739554..ff9ecd7e9d 100644 --- a/internal/core/analyzer/expr.go +++ b/internal/core/analyzer/expr.go @@ -8,31 +8,20 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/ast" ) -// exprType is the analyzer's resolved type for an expression: the -// catalog type OID, nullability, and (when present) the source -// attribute for direct column refs. type exprType struct { typeOID int64 nullable bool sourceClassOID int64 sourceAttributeOID int64 - // sourceTableAlias is the user-visible name of the relation the - // source attribute came from — i.e. the FROM-list alias, or the - // table name when no alias was given. Empty for computed - // expressions. - sourceTableAlias string + sourceTableAlias string } -// typeExpr recursively types an expression, side-effecting parameter -// inference along the way. Returns the expression's exprType. func (a *analyzer) typeExpr(n ast.Node) (exprType, error) { switch e := n.(type) { case nil: return exprType{}, nil case *ast.TODO: - // Parser emits TODO for fields it didn't fully translate (e.g. an - // absent WHERE clause shows up as TODO rather than nil). Treat as no-op. return exprType{}, nil case *ast.A_Const: @@ -57,11 +46,10 @@ func (a *analyzer) typeExpr(n ast.Node) (exprType, error) { return a.typeTypeCast(e) case *ast.NullTest: - // IS [NOT] NULL always returns boolean, regardless of operand type. if _, err := a.typeExpr(e.Arg); err != nil { return exprType{}, err } - return a.boolType(false /* not nullable */) + return a.boolType(false) } return exprType{}, fmt.Errorf("typeExpr: unsupported %T", n) } @@ -82,8 +70,6 @@ func (a *analyzer) typeConst(c *ast.A_Const) (exprType, error) { } return exprType{typeOID: oid}, nil case *ast.String: - // PG bare string literals start as `unknown` but coerce contextually; - // for simplicity we type them as text and rely on casts later. oid, err := a.cat.TypeOID("text") if err != nil { return exprType{}, err @@ -92,7 +78,6 @@ func (a *analyzer) typeConst(c *ast.A_Const) (exprType, error) { case *ast.Boolean: return a.boolType(false) case nil: - // NULL literal return exprType{nullable: true}, nil } return exprType{}, fmt.Errorf("typeConst: unsupported %T", c.Val) @@ -136,8 +121,6 @@ func (a *analyzer) typeColumnRef(c *ast.ColumnRef) (exprType, error) { }, nil } -// flattenFields converts a ColumnRef.Fields list into a slice of strings, -// stopping at any *A_Star. func flattenFields(fields *ast.List) []string { if fields == nil { return nil @@ -164,9 +147,6 @@ func (a *analyzer) typeParamRef(p *ast.ParamRef) (exprType, error) { return exprType{typeOID: cur.TypeOID, nullable: !cur.NotNull}, nil } -// inferParam updates a previously-seen parameter's type from its -// usage context (typically the other side of a binary operator). -// Only fills in when not already set. func (a *analyzer) inferParam(number int, t exprType) { cur, ok := a.params[number] if !ok { @@ -180,10 +160,6 @@ func (a *analyzer) inferParam(number int, t exprType) { } cur.NotNull = !t.nullable } - // Record the source column the parameter binds against (e.g. - // `WHERE age > $1` → users.age). Only set when not yet known so - // the first usage wins; subsequent appearances of the same param - // against a different column don't clobber it. if cur.Source == nil && t.sourceAttributeOID != 0 { ad, err := a.cat.LookupAttribute(t.sourceAttributeOID) if err == nil { @@ -197,9 +173,6 @@ func (a *analyzer) inferParam(number int, t exprType) { } } -// typeAExpr handles binary and unary expressions. For now we only -// implement OP_OP (the standard binary/unary operator case); other -// kinds error out for visibility. func (a *analyzer) typeAExpr(e *ast.A_Expr) (exprType, error) { if e.Kind != ast.A_Expr_Kind_OP { return exprType{}, fmt.Errorf("a_expr: unsupported kind %v", e.Kind) @@ -218,7 +191,6 @@ func (a *analyzer) typeAExpr(e *ast.A_Expr) (exprType, error) { return exprType{}, err } - // Cross-infer parameter types from the non-param side. if pr, ok := e.Lexpr.(*ast.ParamRef); ok && rightT.typeOID != 0 { a.inferParam(pr.Number, rightT) leftT = rightT @@ -251,8 +223,6 @@ func opNameFromList(l *ast.List) string { return strings.Join(parts, ".") } -// resolveOperator finds an operator overload, attempting implicit casts -// on either side if no exact match exists. func (a *analyzer) resolveOperator(name string, leftOID, rightOID int64) (core.OperatorOverload, error) { candidates, err := a.cat.FindOperators(name, leftOID, rightOID) if err != nil { @@ -262,9 +232,6 @@ func (a *analyzer) resolveOperator(name string, leftOID, rightOID int64) (core.O return candidates[0], nil } - // Try implicit-cast both sides: enumerate all overloads of this name - // and pick the first whose operand types are reachable via implicit - // casts from our actual operand types. all, err := a.cat.FindOperators(name, 0, 0) if err != nil { return core.OperatorOverload{}, err @@ -282,7 +249,6 @@ func (a *analyzer) resolveOperator(name string, leftOID, rightOID int64) (core.O continue } } - // Reject candidates with mismatched arity if (leftOID == 0) != (ov.LeftTypeOID == 0) { continue } @@ -304,14 +270,11 @@ func (a *analyzer) typeBoolExpr(b *ast.BoolExpr) (exprType, error) { } func (a *analyzer) typeFuncCall(f *ast.FuncCall) (exprType, error) { - // Function name comes through as a List of *String parts (qualified name) - // in either f.Funcname or f.Func. name := funcCallName(f) if name == "" { return exprType{}, fmt.Errorf("func call: missing name") } - // COUNT(*) always returns bigint and is non-null. if f.AggStar && (name == "count" || name == "count.*") { oid, err := a.cat.TypeOID("int8") if err != nil { @@ -320,8 +283,6 @@ func (a *analyzer) typeFuncCall(f *ast.FuncCall) (exprType, error) { return exprType{typeOID: oid, nullable: false}, nil } - // Type the args (we don't yet resolve overloads against argument types - // — we just take the first overload by name and trust the catalog). for _, arg := range listItems(f.Args) { if _, err := a.typeExpr(arg); err != nil { return exprType{}, err diff --git a/internal/core/analyzer/projection.go b/internal/core/analyzer/projection.go index 067858111e..3ed4034a76 100644 --- a/internal/core/analyzer/projection.go +++ b/internal/core/analyzer/projection.go @@ -5,11 +5,8 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/ast" ) -// projectTarget evaluates one entry in the SELECT list and records an -// output Column. Star expansions emit one column per source column. func (a *analyzer) projectTarget(rt *ast.ResTarget) error { if cr, ok := rt.Val.(*ast.ColumnRef); ok { - // "* " or "t.*" expansion if isStarRef(cr) { a.emitStar(cr) return nil @@ -37,9 +34,6 @@ func (a *analyzer) projectTarget(rt *ast.ResTarget) error { return nil } -// decorateSource fills in the resolved Source struct and per-column -// flag fields for a Column whose value came from a real table column. -// Computed expressions (zero attOID) are left untouched. func (a *analyzer) decorateSource(col *core.Column, attOID int64, tableAlias string) { if attOID == 0 { return @@ -62,9 +56,6 @@ func (a *analyzer) decorateSource(col *core.Column, attOID int64, tableAlias str col.IsAutoIncrement = ad.AutoIncrement } -// targetName picks the user-visible name for a SELECT-list entry: the -// alias if one was given, the source column name for direct refs, or -// the conventional "?column?" placeholder for computed expressions. func targetName(rt *ast.ResTarget) string { if rt.Name != nil && *rt.Name != "" { return *rt.Name @@ -88,7 +79,6 @@ func isStarRef(c *ast.ColumnRef) bool { return len(parts) > 0 && parts[len(parts)-1] == "*" } -// emitStar expands * (or t.*) into one Column per source attribute. func (a *analyzer) emitStar(cr *ast.ColumnRef) { parts := flattenFields(cr.Fields) relName := "" diff --git a/internal/core/analyzer/scope.go b/internal/core/analyzer/scope.go index 1357a6b20f..772d6e2dc5 100644 --- a/internal/core/analyzer/scope.go +++ b/internal/core/analyzer/scope.go @@ -7,22 +7,16 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/ast" ) -// scope is the set of relations available to expression resolution at a -// given point in a query. For now it holds at most one parent (FROM -// list) — subquery / CTE / lateral parents are a follow-up. type scope struct { rels []scopeRel } -// scopeRel binds a FROM-list relation (table or aliased table) to the -// columns it contributes. type scopeRel struct { - alias string // user-visible name for resolution; defaults to relation name + alias string classOID int64 cols []scopeCol } -// scopeCol is a single column visible through a scopeRel. type scopeCol struct { name string attOID int64 @@ -30,9 +24,6 @@ type scopeCol struct { notNull bool } -// buildScope walks a FROM clause (currently: RangeVar plus JoinExpr -// trees, which we flatten into the rels slice) and produces a flat -// scope. Subqueries and table-valued functions are still TODO. func (a *analyzer) buildScope(from *ast.List) (*scope, error) { sc := &scope{} for _, item := range listItems(from) { @@ -53,10 +44,6 @@ func (a *analyzer) appendFromItem(sc *scope, item ast.Node) error { sc.rels = append(sc.rels, rel) return nil case *ast.JoinExpr: - // Flatten both sides into the same scope. The join condition - // (Quals / USING) is type-checked against the assembled scope - // in analyzeSelect after buildScope returns; we don't enforce - // it here. Outer-join nullability is also a follow-up. if err := a.appendFromItem(sc, v.Larg); err != nil { return err } @@ -102,10 +89,6 @@ func (a *analyzer) bindRangeVar(rv *ast.RangeVar) (scopeRel, error) { return rel, nil } -// classColumns reads the column layout of a relation by class OID. It -// goes through the catalog's ClassColumns accessor (backed by a -// sqlc-generated query) rather than TableColumns, because it needs the -// attribute OIDs and resolves by OID rather than by name. func (a *analyzer) classColumns(classOID int64) ([]scopeCol, error) { cols, err := a.cat.ClassColumns(classOID) if err != nil { @@ -123,9 +106,6 @@ func (a *analyzer) classColumns(classOID int64) ([]scopeCol, error) { return out, nil } -// resolveColumn locates a (relation, column) pair in the scope. -// relation may be empty for an unqualified reference; in that case we -// search every relation and error on ambiguity. func (s *scope) resolveColumn(relation, column string) (rel scopeRel, col scopeCol, ok bool, err error) { var matches []struct { rel scopeRel @@ -153,5 +133,4 @@ func (s *scope) resolveColumn(relation, column string) (rel scopeRel, col scopeC return matches[0].rel, matches[0].col, true, nil } -// silence unused-import warning if core not referenced here directly var _ = core.Column{} diff --git a/internal/core/attribute.go b/internal/core/attribute.go index c546a53a64..599479c26c 100644 --- a/internal/core/attribute.go +++ b/internal/core/attribute.go @@ -7,27 +7,21 @@ import ( "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) -// AttributeSpec carries the data needed to register a column on a -// relation. It is the full-fidelity counterpart to CreateAttribute, -// which is kept as a thin wrapper for callers that don't need to set -// any of the "extra" metadata. type AttributeSpec struct { ClassOID int64 Name string TypeOID int64 - Num int // 1-based ordinal position + Num int NotNull bool HasDefault bool - DeclType string // original declared type, before normalization - TypeLength int // varchar(N), numeric(p,s).p, etc. - TypeScale int // numeric(p,s).s + DeclType string + TypeLength int + TypeScale int AutoIncrement bool IsPrimaryKey bool IsUnique bool } -// CreateAttributeSpec inserts a column with the full set of attribute -// metadata. func (c *Catalog) CreateAttributeSpec(s AttributeSpec) error { err := c.q.CreateAttribute(context.Background(), catalogdb.CreateAttributeParams{ ClassOid: s.ClassOID, @@ -49,8 +43,6 @@ func (c *Catalog) CreateAttributeSpec(s AttributeSpec) error { return nil } -// CreateAttribute inserts a column for the given relation. Wraps -// CreateAttributeSpec for callers that only set the basics. func (c *Catalog) CreateAttribute(classOID int64, name string, typeOID int64, notNull bool, hasDefault bool, num int) error { return c.CreateAttributeSpec(AttributeSpec{ ClassOID: classOID, @@ -62,9 +54,6 @@ func (c *Catalog) CreateAttribute(classOID int64, name string, typeOID int64, no }) } -// SetAttributePrimaryKey flips the is_primary_key (and not_null) flag -// on every named column of a relation. Used when a table-level PRIMARY -// KEY constraint is processed after the columns have been registered. func (c *Catalog) SetAttributePrimaryKey(classOID int64, columns []string) error { ctx := context.Background() for _, name := range columns { @@ -79,9 +68,6 @@ func (c *Catalog) SetAttributePrimaryKey(classOID int64, columns []string) error return nil } -// SetAttributeUnique flips is_unique on every named column. Multi-column -// UNIQUE constraints don't make individual columns unique, so callers -// should pass length-1 column lists only. func (c *Catalog) SetAttributeUnique(classOID int64, columns []string) error { ctx := context.Background() for _, name := range columns { @@ -96,7 +82,6 @@ func (c *Catalog) SetAttributeUnique(classOID int64, columns []string) error { return nil } -// ColumnInfo describes a resolved column. type ColumnInfo struct { Name string TypeName string @@ -112,7 +97,6 @@ type ColumnInfo struct { ClassOID int64 } -// ResolveColumn looks up a column by table name and column name. func (c *Catalog) ResolveColumn(table, column string) (*ColumnInfo, error) { r, err := c.q.ResolveColumn(context.Background(), catalogdb.ResolveColumnParams{ TableName: table, @@ -138,7 +122,6 @@ func (c *Catalog) ResolveColumn(table, column string) (*ColumnInfo, error) { return &info, nil } -// TableColumns returns all columns for a given table name, ordered by position. func (c *Catalog) TableColumns(table string) ([]ColumnInfo, error) { rows, err := c.q.TableColumns(context.Background(), table) if err != nil { @@ -164,8 +147,6 @@ func (c *Catalog) TableColumns(table string) ([]ColumnInfo, error) { return cols, nil } -// ClassColumn is a column of a relation identified by class OID, used by -// the analyzer to build its per-relation scope. type ClassColumn struct { AttOID int64 Name string @@ -173,8 +154,6 @@ type ClassColumn struct { NotNull bool } -// ClassColumns returns the columns of a relation by class OID, ordered by -// position. func (c *Catalog) ClassColumns(classOID int64) ([]ClassColumn, error) { rows, err := c.q.ClassAttributes(context.Background(), classOID) if err != nil { @@ -192,16 +171,12 @@ func (c *Catalog) ClassColumns(classOID int64) ([]ClassColumn, error) { return out, nil } -// CodegenColumn is the minimal column shape codegen needs to emit a model -// field: the column name, its resolved type name, and nullability. type CodegenColumn struct { Name string TypeName string NotNull bool } -// ClassCodegenColumns returns the columns of a relation by class OID in the -// shape the codegen catalog projection consumes. func (c *Catalog) ClassCodegenColumns(classOID int64) ([]CodegenColumn, error) { rows, err := c.q.ListClassColumns(context.Background(), classOID) if err != nil { @@ -218,9 +193,6 @@ func (c *Catalog) ClassCodegenColumns(classOID int64) ([]CodegenColumn, error) { return out, nil } -// AttributeDetails describes a column resolved by its OID. Populated -// from sql_attribute joined back to sql_class / sql_namespace, so the -// caller doesn't have to re-resolve names from raw OIDs. type AttributeDetails struct { Schema string Table string @@ -235,10 +207,6 @@ type AttributeDetails struct { NotNull bool } -// LookupAttribute returns the full source-side metadata for a column, -// keyed by attribute OID. Used by analyzers to populate Column.Source -// (and the per-column flag fields) once they've resolved an -// expression to a sql_attribute row. func (c *Catalog) LookupAttribute(attOID int64) (AttributeDetails, error) { r, err := c.q.LookupAttribute(context.Background(), attOID) if err != nil { diff --git a/internal/core/cast.go b/internal/core/cast.go index f0464351e6..408663bf8e 100644 --- a/internal/core/cast.go +++ b/internal/core/cast.go @@ -7,24 +7,14 @@ import ( "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) -// CastSpec describes a type-coercion rule. -// -// Context: -// - 'i' implicit — applied automatically by the analyzer -// - 'a' assignment — applied for INSERT/UPDATE assignments -// - 'e' explicit — only when the user writes CAST or :: -// -// ProcOID == 0 means binary-coercible (no function call required). type CastSpec struct { SourceTypeOID int64 TargetTypeOID int64 ProcOID int64 - Context string // 'i' | 'a' | 'e'; default 'e' + Context string DialectOID int64 } -// CreateCast inserts a cast rule. (source, target) is the primary key, so -// re-inserting the same pair will fail; use ReplaceCast to overwrite. func (c *Catalog) CreateCast(cs CastSpec) error { if cs.Context == "" { cs.Context = "e" @@ -42,8 +32,6 @@ func (c *Catalog) CreateCast(cs CastSpec) error { return nil } -// FindCast returns the cast rule from src to tgt, if one exists, plus -// whether it was found. func (c *Catalog) FindCast(src, tgt int64) (CastSpec, bool, error) { row, err := c.q.FindCast(context.Background(), catalogdb.FindCastParams{ SourceTypeOid: src, @@ -61,8 +49,6 @@ func (c *Catalog) FindCast(src, tgt int64) (CastSpec, bool, error) { }, true, nil } -// CastAllowed reports whether src can be coerced to tgt in the given context. -// Same-type pairs always succeed. func (c *Catalog) CastAllowed(src, tgt int64, ctx string) (bool, error) { if src == tgt { return true, nil diff --git a/internal/core/cast_test.go b/internal/core/cast_test.go index 442ef4ebb6..8e1f7171ce 100644 --- a/internal/core/cast_test.go +++ b/internal/core/cast_test.go @@ -13,13 +13,11 @@ func TestCastAllowed(t *testing.T) { bigintOID, _ := cat.CreateType("bigint", 8) textOID, _ := cat.CreateType("text", 0) - // integer -> bigint is implicit if err := cat.CreateCast(CastSpec{ SourceTypeOID: intOID, TargetTypeOID: bigintOID, Context: "i", }); err != nil { t.Fatal(err) } - // integer -> text is explicit only if err := cat.CreateCast(CastSpec{ SourceTypeOID: intOID, TargetTypeOID: textOID, Context: "e", }); err != nil { @@ -31,14 +29,14 @@ func TestCastAllowed(t *testing.T) { ctx string want bool }{ - {intOID, intOID, "i", true}, // identity always OK - {intOID, bigintOID, "i", true}, // declared implicit - {intOID, bigintOID, "a", true}, // implicit subsumes assignment - {intOID, bigintOID, "e", true}, // implicit subsumes explicit - {intOID, textOID, "i", false}, // explicit-only blocked from implicit - {intOID, textOID, "a", false}, // and from assignment - {intOID, textOID, "e", true}, // but works for explicit - {textOID, intOID, "e", false}, // no rule defined + {intOID, intOID, "i", true}, + {intOID, bigintOID, "i", true}, + {intOID, bigintOID, "a", true}, + {intOID, bigintOID, "e", true}, + {intOID, textOID, "i", false}, + {intOID, textOID, "a", false}, + {intOID, textOID, "e", true}, + {textOID, intOID, "e", false}, } for _, c := range cases { got, err := cat.CastAllowed(c.src, c.tgt, c.ctx) diff --git a/internal/core/catalog.go b/internal/core/catalog.go index 73f3d8587a..e87af5b7d6 100644 --- a/internal/core/catalog.go +++ b/internal/core/catalog.go @@ -1,9 +1,3 @@ -// Package core provides a SQL catalog backed by an in-memory SQLite database. -// -// The catalog stores schema metadata (namespaces, types, tables, columns, -// constraints) in tables prefixed with sql_. Engine packages populate the -// catalog from DDL and then query it to resolve types during statement -// preparation. package core import ( @@ -16,36 +10,19 @@ import ( _ "modernc.org/sqlite" ) -// The catalogdb package is generated by running sqlc against the SQL in -// catalogdef (schema.sql + query.sql) — sqlc analyzing sqlc's catalog. -// Regenerate with `go generate ./internal/core/...`. -// //go:generate go run github.com/sqlc-dev/sqlc/cmd/sqlc generate -// Catalog is an in-memory SQLite database that stores schema metadata in -// sql_* tables modeled after PostgreSQL's system catalogs. -// -// Queries against the sql_* tables are served by the sqlc-generated -// catalogdb.Queries (compiled from catalogdef/query.sql) — sqlc analyzing -// sqlc's own catalog. type Catalog struct { db *sql.DB q *catalogdb.Queries } -// Option configures a Catalog at creation time. The most common use is -// to seed dialect-specific built-ins; see WithSeed. type Option func(*Catalog) error -// WithSeed runs a seed function (typically dialect-provided) against the -// freshly bootstrapped catalog. Failures abort Catalog creation. func WithSeed(fn func(*Catalog) error) Option { return func(c *Catalog) error { return fn(c) } } -// New creates a new in-memory catalog and initializes the sql_* tables. -// Each Option runs after schema initialization and the default-namespace -// bootstrap; an Option that fails aborts catalog creation. func New(opts ...Option) (*Catalog, error) { db, err := sql.Open("sqlite", ":memory:") if err != nil { @@ -69,20 +46,14 @@ func New(opts ...Option) (*Catalog, error) { return c, nil } -// Close closes the underlying database connection. func (c *Catalog) Close() error { return c.db.Close() } -// DB returns the underlying *sql.DB for advanced queries. func (c *Catalog) DB() *sql.DB { return c.db } -// bootstrap seeds the catalog with the default "public" namespace. -// Built-in types and functions are added by per-dialect seed files, -// not here, so that nothing in the catalog is hardcoded to a single -// dialect's view of the world. func (c *Catalog) bootstrap() error { _, err := c.CreateNamespace("public") return err diff --git a/internal/core/catalogdef/embed.go b/internal/core/catalogdef/embed.go index c7de3b8efc..416961f2f2 100644 --- a/internal/core/catalogdef/embed.go +++ b/internal/core/catalogdef/embed.go @@ -1,14 +1,6 @@ -// Package catalogdef holds the SQL that defines sqlc's internal catalog: -// schema.sql (the sql_* table definitions) and query.sql (the queries -// against them). Both are the input to sqlc's own code generation — sqlc -// analyzing sqlc's catalog — and schema.sql is embedded here so the core -// package builds its in-memory catalog from the same source of truth that -// codegen reads. package catalogdef import _ "embed" -// Schema is the DDL that creates the sql_* catalog tables. -// //go:embed schema.sql var Schema string diff --git a/internal/core/class.go b/internal/core/class.go index 4de691cbfd..33e5c69192 100644 --- a/internal/core/class.go +++ b/internal/core/class.go @@ -7,8 +7,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) -// CreateClass inserts a relation (table, view, etc.) and returns its OID. -// Kind should be 'r' (table), 'v' (view), or 'i' (index). func (c *Catalog) CreateClass(namespaceOID int64, name string, kind string) (int64, error) { oid, err := c.q.CreateClass(context.Background(), catalogdb.CreateClassParams{ NamespaceOid: namespaceOID, @@ -21,7 +19,6 @@ func (c *Catalog) CreateClass(namespaceOID int64, name string, kind string) (int return oid, nil } -// ClassOID returns the OID for the given relation in the given namespace. func (c *Catalog) ClassOID(namespaceOID int64, name string) (int64, error) { oid, err := c.q.ClassOID(context.Background(), catalogdb.ClassOIDParams{ NamespaceOid: namespaceOID, @@ -33,8 +30,6 @@ func (c *Catalog) ClassOID(namespaceOID int64, name string) (int64, error) { return oid, nil } -// ClassOIDByName looks up a relation by name across all namespaces. -// If multiple matches exist, it returns the first found. func (c *Catalog) ClassOIDByName(name string) (int64, error) { oid, err := c.q.ClassOIDByName(context.Background(), name) if err != nil { @@ -43,7 +38,6 @@ func (c *Catalog) ClassOIDByName(name string) (int64, error) { return oid, nil } -// DropClass removes a relation and all of its columns from the catalog. func (c *Catalog) DropClass(classOID int64) error { ctx := context.Background() if err := c.q.DeleteAttributesByClass(ctx, classOID); err != nil { @@ -55,13 +49,11 @@ func (c *Catalog) DropClass(classOID int64) error { return nil } -// ClassInfo is a relation row exposed for codegen catalog projection. type ClassInfo struct { OID int64 Name string } -// TablesInNamespace returns the tables (kind 'r') in a namespace, by OID. func (c *Catalog) TablesInNamespace(namespaceOID int64) ([]ClassInfo, error) { rows, err := c.q.ListTablesInNamespace(context.Background(), namespaceOID) if err != nil { diff --git a/internal/core/constraint.go b/internal/core/constraint.go index 08a055d849..d75e3c2abf 100644 --- a/internal/core/constraint.go +++ b/internal/core/constraint.go @@ -7,9 +7,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) -// CreateConstraint inserts a constraint for the given relation. -// Kind should be 'p' (primary key), 'f' (foreign key), 'u' (unique), or 'c' (check). -// columns is a comma-separated list of attribute ordinal positions. func (c *Catalog) CreateConstraint(classOID int64, name string, kind string, columns string) error { err := c.q.CreateConstraint(context.Background(), catalogdb.CreateConstraintParams{ ClassOid: classOID, diff --git a/internal/core/dialect.go b/internal/core/dialect.go index 850d0f5ace..283c0324b4 100644 --- a/internal/core/dialect.go +++ b/internal/core/dialect.go @@ -7,7 +7,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) -// CreateDialect inserts a SQL dialect and returns its OID. func (c *Catalog) CreateDialect(name string) (int64, error) { oid, err := c.q.CreateDialect(context.Background(), name) if err != nil { @@ -16,7 +15,6 @@ func (c *Catalog) CreateDialect(name string) (int64, error) { return oid, nil } -// DialectOID returns the OID for a registered dialect. func (c *Catalog) DialectOID(name string) (int64, error) { oid, err := c.q.DialectOID(context.Background(), name) if err != nil { @@ -25,8 +23,6 @@ func (c *Catalog) DialectOID(name string) (int64, error) { return oid, nil } -// SetDialectFlag stores a per-dialect configuration value. -// If the key already exists, the value is replaced. func (c *Catalog) SetDialectFlag(dialectOID int64, key, value string) error { err := c.q.SetDialectFlag(context.Background(), catalogdb.SetDialectFlagParams{ DialectOid: dialectOID, @@ -39,14 +35,13 @@ func (c *Catalog) SetDialectFlag(dialectOID int64, key, value string) error { return nil } -// DialectFlag returns the value of a dialect flag, or "" if not set. func (c *Catalog) DialectFlag(dialectOID int64, key string) (string, error) { value, err := c.q.DialectFlag(context.Background(), catalogdb.DialectFlagParams{ DialectOid: dialectOID, Key: key, }) if err != nil { - return "", nil // missing flag = empty string, not an error + return "", nil } return value, nil } diff --git a/internal/core/dialect_test.go b/internal/core/dialect_test.go index e3818c3aab..e830a035f4 100644 --- a/internal/core/dialect_test.go +++ b/internal/core/dialect_test.go @@ -27,7 +27,6 @@ func TestDialectAndFlags(t *testing.T) { t.Errorf("DialectFlag: got %q (err=%v), want fold_lower", v, err) } - // upsert if err := cat.SetDialectFlag(pgOID, "identifier_case", "fold_upper"); err != nil { t.Fatal(err) } @@ -36,7 +35,6 @@ func TestDialectAndFlags(t *testing.T) { t.Errorf("upsert: got %q, want fold_upper", v) } - // missing flag returns "" v, err = cat.DialectFlag(pgOID, "nonexistent") if err != nil || v != "" { t.Errorf("missing flag: got %q (err=%v), want empty", v, err) diff --git a/internal/core/namespace.go b/internal/core/namespace.go index 1f83f536a3..b59405336e 100644 --- a/internal/core/namespace.go +++ b/internal/core/namespace.go @@ -5,7 +5,6 @@ import ( "fmt" ) -// CreateNamespace inserts a namespace and returns its OID. func (c *Catalog) CreateNamespace(name string) (int64, error) { oid, err := c.q.CreateNamespace(context.Background(), name) if err != nil { @@ -14,7 +13,6 @@ func (c *Catalog) CreateNamespace(name string) (int64, error) { return oid, nil } -// NamespaceOID returns the OID for the given namespace name. func (c *Catalog) NamespaceOID(name string) (int64, error) { oid, err := c.q.NamespaceOID(context.Background(), name) if err != nil { @@ -23,13 +21,11 @@ func (c *Catalog) NamespaceOID(name string) (int64, error) { return oid, nil } -// NamespaceInfo is a namespace row exposed for codegen catalog projection. type NamespaceInfo struct { OID int64 Name string } -// Namespaces returns every namespace, ordered by OID. func (c *Catalog) Namespaces() ([]NamespaceInfo, error) { rows, err := c.q.ListNamespaces(context.Background()) if err != nil { diff --git a/internal/core/operator.go b/internal/core/operator.go index 4cf2125d94..7eae634aeb 100644 --- a/internal/core/operator.go +++ b/internal/core/operator.go @@ -7,20 +7,16 @@ import ( "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) -// OperatorSpec describes an operator overload for insertion into the catalog. -// A NULL left_type_oid encodes a prefix unary operator; NULL right_type_oid -// encodes a postfix unary. type OperatorSpec struct { Name string - NamespaceOID int64 // 0 = NULL - DialectOID int64 // 0 = NULL (shared) - LeftTypeOID int64 // 0 = NULL - RightTypeOID int64 // 0 = NULL + NamespaceOID int64 + DialectOID int64 + LeftTypeOID int64 + RightTypeOID int64 ResultTypeOID int64 - ProcOID int64 // 0 = NULL + ProcOID int64 } -// CreateOperator inserts an operator overload and returns its OID. func (c *Catalog) CreateOperator(o OperatorSpec) (int64, error) { oid, err := c.q.CreateOperator(context.Background(), catalogdb.CreateOperatorParams{ NamespaceOid: nullInt64(o.NamespaceOID), @@ -37,19 +33,15 @@ func (c *Catalog) CreateOperator(o OperatorSpec) (int64, error) { return oid, nil } -// OperatorOverload is a resolved candidate from FindOperators. type OperatorOverload struct { OID int64 Name string - LeftTypeOID int64 // 0 = NULL (prefix unary) - RightTypeOID int64 // 0 = NULL (postfix unary) + LeftTypeOID int64 + RightTypeOID int64 ResultTypeOID int64 - ProcOID int64 // 0 = NULL (binary-compatible) + ProcOID int64 } -// FindOperators returns all operator overloads matching the given name and -// (left,right) operand types. A 0 type means "any" and skips the filter on -// that side; useful for listing all overloads of a name. func (c *Catalog) FindOperators(name string, leftTypeOID, rightTypeOID int64) ([]OperatorOverload, error) { rows, err := c.q.FindOperators(context.Background(), catalogdb.FindOperatorsParams{ Name: name, diff --git a/internal/core/operator_test.go b/internal/core/operator_test.go index c1631ce35f..1aa5ac045b 100644 --- a/internal/core/operator_test.go +++ b/internal/core/operator_test.go @@ -12,7 +12,6 @@ func TestOperatorCreateAndFind(t *testing.T) { intOID, _ := cat.CreateType("integer", 4) boolOID, _ := cat.CreateType("boolean", 1) - // > (int, int) -> bool gtOID, err := cat.CreateOperator(OperatorSpec{ Name: ">", LeftTypeOID: intOID, @@ -37,7 +36,6 @@ func TestOperatorCreateAndFind(t *testing.T) { t.Errorf("result type: got %d, want %d", got[0].ResultTypeOID, boolOID) } - // listing all overloads of > all, err := cat.FindOperators(">", 0, 0) if err != nil { t.Fatal(err) diff --git a/internal/core/proc.go b/internal/core/proc.go index 58539ac9ba..a726c9f459 100644 --- a/internal/core/proc.go +++ b/internal/core/proc.go @@ -9,30 +9,26 @@ import ( "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) -// ProcSpec describes a function/aggregate/window/procedure for insertion -// into the catalog. type ProcSpec struct { Name string - NamespaceOID int64 // 0 = NULL - DialectOID int64 // 0 = NULL (shared) - Kind string // 'f'unc | 'a'gg | 'w'in | 'p'roc; default 'f' + NamespaceOID int64 + DialectOID int64 + Kind string ReturnTypeOID int64 ReturnSet bool ReturnNullable bool Strict bool - VariadicKind string // 'n'one | 'a'rray | 'v'ariadic-any; default 'n' + VariadicKind string Args []ProcArg } -// ProcArg is a single argument in a proc signature. type ProcArg struct { Name string TypeOID int64 - Mode string // 'i'n | 'o'ut | 'b'oth | 't'able | 'v'ariadic; default 'i' + Mode string HasDefault bool } -// CreateProc inserts a proc and its arguments, returning the proc OID. func (c *Catalog) CreateProc(p ProcSpec) (int64, error) { if p.Kind == "" { p.Kind = "f" @@ -75,7 +71,6 @@ func (c *Catalog) CreateProc(p ProcSpec) (int64, error) { return procOID, nil } -// ProcOverload describes one resolved candidate from FindProcs. type ProcOverload struct { OID int64 Name string @@ -85,9 +80,6 @@ type ProcOverload struct { ArgTypes []int64 } -// FindProcs returns all procs matching the given (case-insensitive) name in -// any of the supplied namespaces. Pass an empty namespace list to search -// every namespace. Argument lists are populated in declaration order. func (c *Catalog) FindProcs(name string, namespaceOIDs []int64) ([]ProcOverload, error) { ctx := context.Background() lname := strings.ToLower(name) diff --git a/internal/core/proc_test.go b/internal/core/proc_test.go index 54ff899bcd..7b69b77956 100644 --- a/internal/core/proc_test.go +++ b/internal/core/proc_test.go @@ -18,7 +18,6 @@ func TestProcCreateAndFind(t *testing.T) { t.Fatal(err) } - // length(text) -> integer procOID, err := cat.CreateProc(ProcSpec{ Name: "length", Kind: "f", @@ -33,7 +32,6 @@ func TestProcCreateAndFind(t *testing.T) { t.Fatal("expected non-zero proc oid") } - // concat(text, text) -> text if _, err := cat.CreateProc(ProcSpec{ Name: "concat", ReturnTypeOID: textOID, diff --git a/internal/core/types.go b/internal/core/types.go index 55a50dd0e5..7515f95372 100644 --- a/internal/core/types.go +++ b/internal/core/types.go @@ -9,28 +9,21 @@ import ( "github.com/sqlc-dev/sqlc/internal/core/catalogdb" ) -// TypeSpec describes a SQL type for insertion into the catalog. -// All fields are optional; Name is required. type TypeSpec struct { Name string Size int - Typtype string // 'b'ase | 'c'omposite | 'd'omain | 'e'num | 'p'seudo | 'r'ange - Category string // 'N'umeric | 'S'tring | 'B'oolean | 'D'atetime | 'A'rray | ... + Typtype string + Category string Preferred bool - NamespaceOID int64 // 0 = NULL - DialectOID int64 // 0 = NULL (shared) - ElementOID int64 // for arrays; 0 = NULL + NamespaceOID int64 + DialectOID int64 + ElementOID int64 } -// CreateType inserts a base type (typtype='b') and returns its OID. -// For full control, use CreateTypeSpec. func (c *Catalog) CreateType(name string, size int) (int64, error) { return c.CreateTypeSpec(TypeSpec{Name: name, Size: size, Typtype: "b"}) } -// CreateTypeSpec inserts a type with full metadata. NamespaceOID defaults -// to the "public" namespace when zero so the simpler CreateType API and -// engines that don't track namespaces (sqlite) continue to work. func (c *Catalog) CreateTypeSpec(t TypeSpec) (int64, error) { if t.Typtype == "" { t.Typtype = "b" @@ -58,10 +51,6 @@ func (c *Catalog) CreateTypeSpec(t TypeSpec) (int64, error) { return oid, nil } -// TypeOID returns the OID for the given (unqualified) type name. When -// the same name lives in multiple namespaces (e.g. a system-internal -// duplicate in information_schema and pg_catalog), the lookup prefers -// pg_catalog, then public, then any other namespace by name. func (c *Catalog) TypeOID(name string) (int64, error) { oid, err := c.q.TypeOIDByName(context.Background(), strings.ToLower(name)) if err != nil { @@ -70,7 +59,6 @@ func (c *Catalog) TypeOID(name string) (int64, error) { return oid, nil } -// TypeName returns the name for the given type OID. func (c *Catalog) TypeName(oid int64) (string, error) { name, err := c.q.TypeNameByOID(context.Background(), oid) if err != nil { @@ -79,7 +67,6 @@ func (c *Catalog) TypeName(oid int64) (string, error) { return name, nil } -// TypeInfo bundles the metadata returned by LookupType. type TypeInfo struct { OID int64 Name string @@ -88,7 +75,6 @@ type TypeInfo struct { Preferred bool } -// LookupType returns metadata for the given type OID. func (c *Catalog) LookupType(oid int64) (TypeInfo, error) { row, err := c.q.LookupType(context.Background(), oid) if err != nil { @@ -103,8 +89,6 @@ func (c *Catalog) LookupType(oid int64) (TypeInfo, error) { }, nil } -// nullableOID / nullableString / boolToInt build the loosely-typed -// arguments the not-yet-migrated raw-SQL call sites pass to database/sql. func nullableOID(oid int64) any { if oid == 0 { return nil @@ -126,8 +110,6 @@ func boolToInt(b bool) int { return 0 } -// nullInt64 / nullString / boolToInt64 build the sql.Null* and int64 -// arguments the sqlc-generated catalogdb queries expect. func nullInt64(oid int64) sql.NullInt64 { return sql.NullInt64{Int64: oid, Valid: oid != 0} } @@ -143,8 +125,6 @@ func boolToInt64(b bool) int64 { return 0 } -// orZero unwraps a nullable OID column, mapping NULL to the 0 sentinel -// the catalog's Go API uses for "unset". func orZero(n sql.NullInt64) int64 { if n.Valid { return n.Int64 diff --git a/internal/engine/clickhouse/analyze_test.go b/internal/engine/clickhouse/analyze_test.go index 8d0369cf59..82563223e3 100644 --- a/internal/engine/clickhouse/analyze_test.go +++ b/internal/engine/clickhouse/analyze_test.go @@ -9,11 +9,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/engine/clickhouse" ) -// analyzeOne seeds a ClickHouse-dialect catalog, applies ddl, then runs -// the dialect-neutral core analyzer over the trailing query. It proves -// the full ClickHouse vertical path: ClickHouse SQL -> sqlc's ClickHouse -// parser -> internal/sql/ast -> core catalog + analyzer -> PrepareResult, -// with no legacy compiler analyze step involved. func analyzeOne(t *testing.T, ddl, query string) core.PrepareResult { t.Helper() cat, err := core.New(clickhouse.Dialect()) @@ -73,7 +68,6 @@ func TestClickHouseSelectColumns(t *testing.T) { if !ok || name.DataType != "string" || !name.NotNull { t.Errorf("name: %+v (ok=%v)", name, ok) } - // Nullable(String) must resolve to the inner scalar and be nullable. tag, ok := colByName(res, "tag") if !ok || tag.DataType != "string" || tag.NotNull { t.Errorf("tag: want string/nullable, got %+v (ok=%v)", tag, ok) diff --git a/internal/engine/clickhouse/convert.go b/internal/engine/clickhouse/convert.go index aef7dd6419..bc4053bb10 100644 --- a/internal/engine/clickhouse/convert.go +++ b/internal/engine/clickhouse/convert.go @@ -826,12 +826,6 @@ func (c *cc) convertColumnDeclaration(n *chast.ColumnDeclaration) *ast.ColumnDef } if n.Type != nil { - // Render the full type text (including nested type parameters such - // as the inner type of Nullable(T) / Array(T) and the precision of - // Decimal(P, S)) into Name so downstream consumers can recover the - // complete declaration. Nested type parameters are themselves - // *chast.DataType nodes, which the generic expression converter - // cannot represent, so they are rendered here instead. colDef.TypeName = &ast.TypeName{ Name: renderDataType(n.Type), } @@ -855,9 +849,6 @@ func (c *cc) convertColumnDeclaration(n *chast.ColumnDeclaration) *ast.ColumnDef return colDef } -// renderDataType renders a ClickHouse type node back to its canonical -// textual form, e.g. Nullable(String), Array(UInt64), Decimal(18, 4), or -// Map(String, Array(Nullable(UInt32))). Nested type parameters recurse. func renderDataType(dt *chast.DataType) string { if dt == nil { return "" @@ -872,9 +863,6 @@ func renderDataType(dt *chast.DataType) string { return dt.Name + "(" + strings.Join(parts, ", ") + ")" } -// renderTypeParam renders a single type parameter: a nested type, a -// numeric/string literal (Decimal precision, FixedString length, Enum -// value), or an identifier. func renderTypeParam(e chast.Expression) string { switch v := e.(type) { case *chast.DataType: diff --git a/internal/engine/clickhouse/parse.go b/internal/engine/clickhouse/parse.go index 83f9488dfe..fa77fd91ef 100644 --- a/internal/engine/clickhouse/parse.go +++ b/internal/engine/clickhouse/parse.go @@ -29,16 +29,10 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { return nil, err } - // doubleclick exposes each statement's start (Pos, 1-based) but not its - // end (End == Pos). Reconstruct byte spans the way the other engines do: - // StmtLocation is a running offset that starts at the end of the - // previous statement, so leading comments (including the sqlc "-- name:" - // annotation) fall inside the statement's span, and StmtLen runs through - // the terminating semicolon. var stmts []ast.Statement loc := 0 for _, stmt := range stmtNodes { - start := stmt.Pos().Offset - 1 // Pos is 1-based; convert to a byte index + start := stmt.Pos().Offset - 1 if start < loc { start = loc } @@ -64,10 +58,6 @@ func (p *Parser) Parse(r io.Reader) ([]ast.Statement, error) { return stmts, nil } -// statementEnd returns the byte index just past the terminating semicolon -// of the statement beginning at start, or len(blob) if there is none. It -// skips over string / quoted-identifier literals and comments so that a -// semicolon inside them does not end the statement early. func statementEnd(blob []byte, start int) int { for i := start; i < len(blob); i++ { switch blob[i] { @@ -90,18 +80,15 @@ func statementEnd(blob []byte, start int) int { return len(blob) } -// skipQuoted returns the index of the closing quote of the literal that -// opens at i (blob[i] is the opening quote). Backslash escapes and doubled -// quotes are honored. func skipQuoted(blob []byte, i int) int { q := blob[i] for j := i + 1; j < len(blob); j++ { switch blob[j] { case '\\': - j++ // skip the escaped byte + j++ case q: if j+1 < len(blob) && blob[j+1] == q { - j++ // doubled quote is an escaped quote + j++ continue } return j diff --git a/internal/engine/clickhouse/schema.go b/internal/engine/clickhouse/schema.go index a2ffad38be..cb37a15bdf 100644 --- a/internal/engine/clickhouse/schema.go +++ b/internal/engine/clickhouse/schema.go @@ -8,9 +8,6 @@ import ( "github.com/sqlc-dev/sqlc/internal/sql/ast" ) -// LoadSchema parses ClickHouse DDL and applies every schema-altering -// statement to the core catalog. It is the ClickHouse entry point for -// populating a catalog from migration files. func LoadSchema(cat *core.Catalog, ddl string) error { stmts, err := NewParser().Parse(strings.NewReader(ddl)) if err != nil { @@ -24,10 +21,6 @@ func LoadSchema(cat *core.Catalog, ddl string) error { return nil } -// Apply walks one parsed ClickHouse statement and applies it to the -// catalog when it is schema-altering (CREATE TABLE, DROP TABLE). -// Non-DDL and unsupported DDL forms are ignored so a mixed DDL/DML -// stream can be handed to the catalog without pre-filtering. func Apply(cat *core.Catalog, n ast.Node) error { switch v := n.(type) { case nil: @@ -71,9 +64,6 @@ func applyCreateTable(cat *core.Catalog, stmt *ast.CreateTableStmt) error { if col == nil || col.TypeName == nil { return fmt.Errorf("clickhouse: column %d on %q: missing type", i+1, stmt.Name.Name) } - // TODO: the core catalog does not yet model array-ness on an - // attribute; for now an Array(T) column is stored as its element - // type T. Preserving the array wrapper is a follow-up. typeName, _, _ := unwrapType(col.TypeName) typeOID, err := resolveOrCreateType(cat, typeName) if err != nil { @@ -120,10 +110,6 @@ func applyDropTable(cat *core.Catalog, stmt *ast.DropTableStmt) error { return nil } -// nsName maps an empty schema to the catalog's default namespace. The -// analyzer resolves unqualified table references against "public", so -// ClickHouse's "default" database is mapped onto it for now. Wiring a -// per-dialect default namespace is a follow-up. func nsName(schema string) string { if schema == "" { return "public" @@ -139,9 +125,6 @@ func resolveOrCreateNamespace(cat *core.Catalog, schema string) (int64, error) { return cat.CreateNamespace(name) } -// resolveOrCreateType looks a type up by name, registering it as an -// unknown user type if the seed did not provide it (e.g. an Enum with -// inline variants, or a type name we have not catalogued yet). func resolveOrCreateType(cat *core.Catalog, name string) (int64, error) { canonical := strings.ToLower(strings.TrimSpace(name)) if canonical == "" { @@ -153,13 +136,6 @@ func resolveOrCreateType(cat *core.Catalog, name string) (int64, error) { return cat.CreateType(canonical, 0) } -// unwrapType reduces a ClickHouse column type to the effective scalar -// type used for storage, reporting whether it is an array and whether it -// is nullable. The ClickHouse converter renders the full type text into -// TypeName.Name (e.g. "Nullable(Array(UInt64))"), which is parsed here. -// It unwraps any nesting of Nullable / LowCardinality / Array; the -// remaining scalar (String, UInt64, Decimal(18, 4), ...) is returned as -// its bare type name, lower-cased. func unwrapType(tn *ast.TypeName) (name string, isArray, nullable bool) { return unwrapTypeString(tn.Name) } @@ -185,15 +161,10 @@ func unwrapTypeString(s string) (name string, isArray, nullable bool) { } return strings.ToLower(base), true, false default: - // Scalar type. Drop any (length/precision) modifiers for the stored - // type name; capturing them into TypeLength/TypeScale is a follow-up. return strings.ToLower(base), false, false } } -// splitType breaks "Name(arg1, arg2, ...)" into its base name and -// top-level arguments, respecting nested parentheses. A type with no -// parentheses returns (name, nil). func splitType(s string) (base string, args []string) { s = strings.TrimSpace(s) open := strings.IndexByte(s, '(') diff --git a/internal/engine/clickhouse/seed.go b/internal/engine/clickhouse/seed.go index af7d149875..986838285c 100644 --- a/internal/engine/clickhouse/seed.go +++ b/internal/engine/clickhouse/seed.go @@ -6,65 +6,37 @@ import ( "github.com/sqlc-dev/sqlc/internal/core" ) -// Dialect returns a core.Option that registers the "clickhouse" dialect -// and seeds the catalog with the built-in ClickHouse data types. It is -// the ClickHouse counterpart to the per-dialect Dialect() helpers in -// xqlc, and is how a ClickHouse-backed core.Catalog is constructed: -// -// cat, err := core.New(clickhouse.Dialect()) func Dialect() core.Option { return core.WithSeed(Seed) } -// chType pairs a ClickHouse type name with its pg_type-style category so -// the catalog can reason about it during (future) operator and cast -// resolution. Names are stored lower-cased by the catalog. type chType struct { name string - category string // 'N'umeric | 'S'tring | 'B'oolean | 'D'atetime | 'A'rray | 'U'serdef + category string } -// clickhouseTypes is the built-in type surface. It is intentionally a -// curated hand list for now; once testgen/clickhouse exists these will -// be regenerated against a real ClickHouse instance, matching the -// seedgen approach used by the other engines. var clickhouseTypes = []chType{ - // Unsigned integers {"UInt8", "N"}, {"UInt16", "N"}, {"UInt32", "N"}, {"UInt64", "N"}, {"UInt128", "N"}, {"UInt256", "N"}, - // Signed integers {"Int8", "N"}, {"Int16", "N"}, {"Int32", "N"}, {"Int64", "N"}, {"Int128", "N"}, {"Int256", "N"}, - // Floating point / decimal {"Float32", "N"}, {"Float64", "N"}, {"BFloat16", "N"}, {"Decimal", "N"}, {"Decimal32", "N"}, {"Decimal64", "N"}, {"Decimal128", "N"}, {"Decimal256", "N"}, - // Boolean {"Bool", "B"}, - // Strings / binary {"String", "S"}, {"FixedString", "S"}, {"UUID", "S"}, - // Date & time {"Date", "D"}, {"Date32", "D"}, {"DateTime", "D"}, {"DateTime64", "D"}, - // Network / misc scalar {"IPv4", "S"}, {"IPv6", "S"}, {"JSON", "U"}, - // Enums are declared with their variants; the base names still resolve. {"Enum8", "U"}, {"Enum16", "U"}, - // Wrapper / composite type names. Columns declared with these are - // normally unwrapped to their inner scalar by the DDL handler, but we - // register the names so an un-unwrappable declaration still resolves. {"Nullable", "U"}, {"LowCardinality", "U"}, {"Array", "A"}, {"Map", "U"}, {"Tuple", "U"}, {"Nested", "U"}, {"Nothing", "U"}, } -// Seed registers the ClickHouse dialect row and its built-in types on a -// freshly bootstrapped catalog. func Seed(cat *core.Catalog) error { dialectOID, err := cat.CreateDialect("clickhouse") if err != nil { return err } - // ClickHouse identifiers are case-sensitive, so unlike MySQL/SQLite we - // do not set a case-folding flag. for _, t := range clickhouseTypes { if _, err := cat.CreateTypeSpec(core.TypeSpec{ Name: t.name,