diff --git a/.gitignore b/.gitignore index 388729e..d2d7727 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,8 @@ target/ # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + +.env +*.db +*.db-shm +*.db-wal diff --git a/.sqlx/query-041f83c1714065658202c4f813761082046d5a31583dffe3037b8773721fb0c6.json b/.sqlx/query-041f83c1714065658202c4f813761082046d5a31583dffe3037b8773721fb0c6.json new file mode 100644 index 0000000..e93a047 --- /dev/null +++ b/.sqlx/query-041f83c1714065658202c4f813761082046d5a31583dffe3037b8773721fb0c6.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT current_value FROM id_sequences WHERE id = 0", + "describe": { + "columns": [ + { + "name": "current_value", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "041f83c1714065658202c4f813761082046d5a31583dffe3037b8773721fb0c6" +} diff --git a/.sqlx/query-7e3e214cd8404407352f23114db11e2caceea8e563f29cbfecb2d2abaaee510c.json b/.sqlx/query-7e3e214cd8404407352f23114db11e2caceea8e563f29cbfecb2d2abaaee510c.json new file mode 100644 index 0000000..8150aa3 --- /dev/null +++ b/.sqlx/query-7e3e214cd8404407352f23114db11e2caceea8e563f29cbfecb2d2abaaee510c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE id_sequences SET current_value = ? WHERE id = 0", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "7e3e214cd8404407352f23114db11e2caceea8e563f29cbfecb2d2abaaee510c" +} diff --git a/.sqlx/query-a80ed7123532bfaf7e8360623ea8f491a37dca665205cba7d69d0c9a657a16d9.json b/.sqlx/query-a80ed7123532bfaf7e8360623ea8f491a37dca665205cba7d69d0c9a657a16d9.json new file mode 100644 index 0000000..10a6f5c --- /dev/null +++ b/.sqlx/query-a80ed7123532bfaf7e8360623ea8f491a37dca665205cba7d69d0c9a657a16d9.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "UPDATE id_sequences SET current_value = current_value + 1 where id = 0 RETURNING current_value", + "describe": { + "columns": [ + { + "name": "current_value", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "a80ed7123532bfaf7e8360623ea8f491a37dca665205cba7d69d0c9a657a16d9" +} diff --git a/Cargo.lock b/Cargo.lock index 02e060e..8107755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,41 +2,1511 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" + +[[package]] +name = "cc" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + [[package]] name = "optd-core" version = "0.1.0" +dependencies = [ + "anyhow", + "async-recursion", + "dotenvy", + "proc-macro2", + "serde", + "serde_json", + "sqlx", + "tokio", + "trait-variant", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" +dependencies = [ + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", - "trait-variant", + "quote", + "unicode-ident", ] [[package]] -name = "proc-macro2" -version = "1.0.60" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ - "unicode-ident", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "quote" -version = "1.0.26" +name = "tempfile" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +dependencies = [ + "cfg-if", + "fastrand", + "getrandom 0.3.1", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", + "quote", + "syn", ] [[package]] -name = "syn" -version = "2.0.0" +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff13bb1732bccfe3b246f3fdb09edfd51c01d6f5299b7ccd9457c2e4e37774" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", ] [[package]] @@ -50,8 +1520,364 @@ dependencies = [ "syn", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/doc.md b/doc.md new file mode 100644 index 0000000..ec234e2 --- /dev/null +++ b/doc.md @@ -0,0 +1,469 @@ +# OPTD Language Specification + +## 1. Type System + +### 1.1 Basic Types +The language provides a fixed set of primitive types for use in operator metadata: + +- `int64`: 64-bit signed integer, range [-2^63, 2^63-1] +- `string`: UTF-8 encoded string of arbitrary length +- `bool`: Boolean value (true/false) + +### 1.2 Composite Types + +#### 1.2.1 Arrays +An array type is denoted as `Array` where T is any valid type. Arrays represent variable-length sequences of values of type T. + + +#### 1.2.2 Enums +Enumerated types allow users to define custom sum types. Each variant in an enum can carry zero or more fields of any valid type T. The syntax is as follows: + +``` +ENUM TypeName + Variant1 # No fields + Variant2(int64, string) # Two fields + Variant3( + ENUM NestedEnum # Nested enum definition + SubVariant1(float64) + SubVariant2(bool, string) + END, + Array # Second field is an array + ) +END +``` + +Enum definitions must follow these rules: +- The enum name must be unique in the type system +- Each variant name must be unique within its enum +- Variant fields can be: + - Any basic type (int64, uint64, float64, string, bool) + - Another enum type (as long as no recursion / cycles) + - An array type of any valid type +- A variant must have zero or more fields, each with an explicit type +- Nested enum definitions are allowed and can be used as field types +- Recursive or cyclic enum definitions are not permitted + +### 1.2.3 Optional + +An optional type is denoted as `Optional`, where T is any valid type. + +## 2. Operator System + +### 2.1 Operator Categories +The system defines two distinct categories of operators: +- Scalar operators: Can only have scalar children +- Logical operators: Can have both scalar and logical children + +### 2.2 Operator Definition Structure + +An operator defines its category (SCALAR or LOGICAL) followed by optional sections for metadata, logical children (for logical operators), and scalar children. Each section can contain any number of fields, including zero. There are no required fields or sections. + +Fields can be variable size `ARRAY`, or `Optional`. + +``` +[CATEGORY] OperatorName + DOC: "Documentation string" + METADATA + # Any number of metadata fields (or none) + field1: Type1 + field2: Type2 + ... + LOGICAL_CHILDREN # Only valid for logical operators + # Any number of logical children (or none) + child1: Logical + child2: Array + ... + SCALAR_CHILDREN + # Any number of scalar children (or none) + child1: Optional + child2: Array + ... +END +``` + +The only constraints are: +- SCALAR operators cannot have LOGICAL_CHILDREN section +- Each field name within a section must be unique +- All types must be valid according to the type system + +For example, these are all valid operators: +``` +LOGICAL Join + # A join with only logical children + LOGICAL_CHILDREN + left: Logical + right: Logical + SCALAR_CHILDREN + predicate: Option + +SCALAR Constant + # A constant with only metadata + METADATA + value: float64 + +LOGICAL Project + # A project with both scalar children and metadata + METADATA + schema: string + SCALAR_CHILDREN + expressions: Array +``` + +### 2.3 Operator Child Constraints +- Scalar operators: + - Cannot have logical children + - Can have zero or more scalar children + +- Logical operators: + - Can have both logical and scalar children + - Can have fixed or variable number of either type + +## 3. Rules and Analysis + +### 3.1 Transformations and Analysis + +OPTD supports two fundamental kinds of operations: + +1. Rules: Transform partial plans into new partial plans + - LogicalRule: PartialLogicalPlan → PartialLogicalPlan + - ScalarRule: PartialScalarPlan → PartialScalarPlan + +2. Analyzers: Extract information from partial plans + - LogicalAnalyzer: PartialLogicalPlan → UserType + - ScalarAnalyzer: PartialScalarPlan → UserType + +Both Rules and Analyzers share the same pattern matching mechanism but differ in their output types. This distinction makes it clearer when we're transforming structure versus computing properties. + +Future extensions: +- Pure functions (UserType → UserType) that operate on analysis results without pattern matching +- Combined analyzers that work on both logical and scalar plans + +Example: +``` +# A Rule transforms structure +RULE join_commute: + MATCH: Join(...) + TRANSFORM: Join(...) # Returns new plan + +# An Analyzer extracts information +ANALYZER get_table_refs: + MATCH: Filter(...) + ANALYZE: TableRefs(...) # Returns user type +``` + +This separation makes it clearer that we have two distinct operations: partial plan transformation and partial plan analysis, even though they use the same matching machinery. + +### 3.2 Pattern Matching + +The pattern matching system distinguishes between three distinct types of patterns, each tailored to match different components of the optimization graph: + +#### Logical Patterns +Logical patterns match against logical operators in the plan graph. Their structure encompasses: +``` +LogicalPattern ::= + | ANY # Matches any logical operator + | Bind(name, pattern) # Binds a logical subplan + | NOT(pattern) # Negative logical matching + | Operator { # Matches specific logical operator + op_type: string, # Operator type to match + metadata: TypePattern[],# Metadata patterns + logical_children: LogicalPattern[], # Logical child patterns + scalar_children: ScalarPattern[] # Scalar child patterns + } +``` + +#### Scalar Patterns +Scalar patterns match against scalar expressions, with a more restricted structure: +``` +ScalarPattern ::= + | ANY # Matches any scalar expression + | Bind(name, pattern) # Binds a scalar subplan + | Operator { # Matches specific scalar operator + op_type: string, # Operator type to match + metadata: TypePattern, # Metadata pattern + scalar_children: ScalarPattern[] # Only scalar children allowed + } +``` + +#### Type Patterns +OPTD type patterns handle matching against metadata values: +``` +TypePattern ::= + | ANY # Matches any metadata value + | Bind(name, value) # Binds a metadata value +``` + +This three-tier pattern system ensures type safety throughout the matching process. Each pattern type enforces appropriate constraints: +- Logical patterns can match both logical and scalar children +- Scalar patterns can only match scalar children +- OPTD type patterns match leaf values in metadata + +For example, matching a join operator with specific metadata would look like: +``` +Operator { + op_type: "Join", + metadata: [Bind("join_type", "Inner")], + logical_children: [ + Bind("left", ANY), + Bind("right", ANY) + ], + scalar_children: [ + Bind("condition", ANY) + ] +} +``` + +### 3.3 Rule Composition + +The WITH clause enables rules to build complex transformations by composing simpler rules. This composition mechanism allows rules to analyze subplans and use the results in their final transformation. + +Rule composition follows this structure: +``` +WITH: + result1 = rule1(ref, arg1, arg2) + result2 = rule2(another_ref) +``` + +Each composition statement: +1. Names the result for later use +2. Specifies which rule to apply +3. Provides a reference as its first argument +4. Optionally provides additional arguments specific to that rule + +The reference must be either: +- A pattern binding from the MATCH phase +- A result from a previous rule application + +The additional arguments are always user defined types (expressions allowed). + +Rule applications are evaluated in order, with each result available to subsequent applications. + +### 3.4 Output Expressions +Output expressions define how Rules and Analyzers produce their results. The system distinguishes between three types... + +#### Type Applications +These expressions operate on and produce optd types: +``` +TypeExpr ::= + | TypeRef(String) # Reference to bound optd type + | IfThenElse { + condition: TypeExpr, + then_branch: TypeExpr, + else_branch: TypeExpr + } + | Eq { # Value equality comparison + left: TypeExpr, + right: TypeExpr + } + | Match { # Pattern matching on optd types + expr: TypeExpr, + cases: [(TypeExpr, TypeExpr)] + } +``` + +#### Scalar Applications +These expressions construct scalar operators, but their conditions must be optd type expressions: +``` +ScalarExpr ::= + | ScalarRef(String) # Reference to bound scalar + | IfThenElse { + condition: TypeExpr, # Condition must be optd type + then_branch: ScalarExpr, + else_branch: ScalarExpr + } + | Match { # Pattern match on optd type + expr: TypeExpr, + cases: [(TypeExpr, ScalarExpr)] + } +``` + +#### Logical Applications +Similar to scalar applications, logical expressions construct plan operators: +``` +LogicalExpr ::= + | LogicalRef(String) # Reference to bound logical plan + | IfThenElse { + condition: TypeExpr, # Condition must be optd type + then_branch: LogicalExpr, + else_branch: LogicalExpr + } + | Match { # Pattern match on optd type + expr: TypeExpr, + cases: [(TypeExpr, LogicalExpr)] + } +``` + +Key constraints: +1. Conditions in all control flow constructs must be optd type expressions +2. Each application type can only reference bindings of its corresponding type +3. Match expressions must have consistent result types across all cases +4. All application expressions within a single rule must produce the same type + +This strict typing ensures that transformations remain type-safe while enabling sophisticated control flow based on analysis results. + +### 3.5 Complete Examples + +### 3.5 Rule Examples + +Let's examine three progressively complex examples that demonstrate the key capabilities of the rule system. + +#### Basic Plan Transformation: Join Commutativity + +The simplest rules perform pure structural transformations on plans. This join commutativity rule demonstrates basic pattern matching and plan reconstruction: + +``` +RULE join_commute: + MATCH: Join( + metadata: { + join_type: "Inner" + }, + logical_children: { + left: Bind("left", ANY), + right: Bind("right", ANY) + }, + scalar_children: { + condition: Bind("cond", ANY) + } + ) + APPLY: Join( + metadata: { + join_type: "Inner" + }, + logical_children: { + left: Ref("right"), + right: Ref("left") + }, + scalar_children: { + condition: Ref("cond") + } + ) +``` + +This rule only swaps the children of inner joins, preserving the join condition and type. It demonstrates basic pattern matching and transformation without needing additional analysis or control flow. + +#### Recursive Analysis: Constant Folding + +The constant folding rule shows how recursion and custom types enable expression analysis and transformation: + +``` +RULE constant_fold: + # Match addition of two expressions + MATCH: Bind("add", Add { + scalar_children: { + left: Bind("left", ANY), + right: Bind("right", ANY) + } + }) + WITH: + left_val = constant_fold(Ref("left")) + right_val = constant_fold(Ref("right")) + APPLY: left_val + right_val + + # Base case - match an integer constant + MATCH: Bind("const", Constant { + metadata: { + value: Bind("val", ANY) + } + }) + APPLY: Ref("val") # Simply return the integer value + ``` + +This rule recursively evaluates expressions, producing an analysis result that tracks constant values. The transformation uses pattern matching on the analysis results to determine whether folding is possible. + +#### Complex Analysis and Transformation: Filter Pushdown + +``` +# Extract table references from expressions like ((A.d = 4) AND (B.c = 6) AND ...) +ANALYZER get_table_refs: + MATCH: Eq { + scalar_children: { + left: Bind("col", + ColumnRef { + metadata: { + table: Bind("table", ANY) + }, + }) + right: TYPE(Constant) + } + } + WITH: + left_refs = get_table_refs(Ref("left")) + right_refs = get_table_refs(Ref("right")) + RETURN: concat(left_refs, right_refs) # Combine table references + + MATCH: And { + scalar_children: { + left: Bind("left", ANY), + right: Bind("right", ANY) + } + } + WITH: + left_refs = get_table_refs(Ref("left")) + right_refs = get_table_refs(Ref("right")) + RETURN: concat(left_refs, right_refs) # Combine table references + +# Main filter pushdown rule +RULE filter_pushdown: + MATCH: Filter( + logical_children: { + input: Join( + logical_children: { + left: Bind("left", ANY), + right: Bind("right", ANY) + } + scalar_children: { + predicate: Bind("join_pred", ANY) + } + ) + }, + scalar_children: { + predicate: Bind("filter_pred", ANY) + } + ) + WITH: + pred_refs = get_table_refs(Ref("filter_pred")) + join_refs = get_table_refs(Ref("join_pred")) + APPLY: + IF pred_refs IN join_refs THEN + Join( + logical_children: { + left: Ref("left"), + right: Ref("right") + } + scalar_children: { + predicate: And { + left: Ref("join_pred"), + right: Ref("filter_pred") + } + } + ) + ELSE + None # Rule fails +``` +This last example might contain lots of boilerplate code, and only pushes down if all references match, but we could come up with more intricate rules. + +### 3.6 Execution Model + +OPTD executes Rules and Analyzers following the same pattern matching process but with distinct outputs: + +#### Rule Execution +1. Match input plan against each pattern in sequence +2. For first successful match: + - Bind pattern variables to matched partial plans + - Execute WITH clause applications in order + - Evaluate TRANSFORM expression with control flow +3. If any step fails (pattern match, WITH clause, or TRANSFORM), try next pattern +4. If no patterns succeed, rule application fails +5. Success produces a new partial plan + +#### Analyzer Execution +1. Match input plan against each pattern in sequence +2. For first successful match: + - Bind pattern variables to matched partial plans + - Execute WITH clause applications in order + - Evaluate ANALYZE expression with control flow +3. If any step fails (pattern match, WITH clause, or ANALYZE), try next pattern +4. If no patterns succeed, analyzer application fails +5. Success produces a user-defined type + +Both Rules and Analyzers share the same pattern matching and composition mechanisms, differing only in their output types. This unified execution model enables composition of transformations and analysis while maintaining type safety through the optimization pipeline. \ No newline at end of file diff --git a/optd-core/Cargo.toml b/optd-core/Cargo.toml index 498e2d0..f464e87 100644 --- a/optd-core/Cargo.toml +++ b/optd-core/Cargo.toml @@ -4,7 +4,14 @@ version = "0.1.0" edition = "2021" [dependencies] +sqlx = { version = "0.8", features = [ "sqlite", "runtime-tokio", "migrate" ] } trait-variant = "0.1.2" # Pin more recent versions for `-Zminimal-versions`. proc-macro2 = "1.0.60" # For a missing feature (https://github.com/rust-lang/rust/issues/113152). +anyhow = "1.0.95" +tokio = { version = "1.43.0", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = { version = "1", features = ["raw_value"] } +dotenvy = "0.15" +async-recursion = "1.1.1" diff --git a/optd-core/src/cascades/expressions.rs b/optd-core/src/cascades/expressions.rs new file mode 100644 index 0000000..ddc9274 --- /dev/null +++ b/optd-core/src/cascades/expressions.rs @@ -0,0 +1,35 @@ +//! Types for logical and physical expressions in the optimizer. + +use crate::operators::relational::physical::PhysicalOperator; +use crate::operators::scalar::ScalarOperator; +use crate::{operators::relational::logical::LogicalOperator, values::OptdValue}; +use serde::Deserialize; + +use super::groups::{RelationalGroupId, ScalarGroupId}; + +/// A logical expression in the memo table. +pub type LogicalExpression = LogicalOperator; + +/// A physical expression in the memo table. +pub type PhysicalExpression = PhysicalOperator; + +/// A scalar expression in the memo table. +pub type ScalarExpression = ScalarOperator; + +/// A unique identifier for a logical expression in the memo table. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Deserialize)] +#[sqlx(transparent)] +pub struct LogicalExpressionId(pub i64); + +/// A unique identifier for a physical expression in the memo table. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Deserialize)] +#[sqlx(transparent)] +pub struct PhysicalExpressionId(pub i64); + +/// A unique identifier for a scalar expression in the memo table. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type, Deserialize)] +#[sqlx(transparent)] +pub struct ScalarExpressionId(pub i64); diff --git a/optd-core/src/cascades/groups.rs b/optd-core/src/cascades/groups.rs new file mode 100644 index 0000000..ae94385 --- /dev/null +++ b/optd-core/src/cascades/groups.rs @@ -0,0 +1,49 @@ +use serde::Deserialize; + +/// A unique identifier for a group of relational expressions in the memo table. +#[repr(transparent)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + sqlx::Type, + serde::Serialize, + Deserialize, +)] +#[sqlx(transparent)] +pub struct RelationalGroupId(pub i64); + +/// A unique identifier for a group of scalar expressions in the memo table. +#[repr(transparent)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + sqlx::Type, + serde::Serialize, + Deserialize, +)] +#[sqlx(transparent)] +pub struct ScalarGroupId(pub i64); + +/// The exploration status of a group or a logical expression in the memo table. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::Type)] +#[repr(i32)] +pub enum ExplorationStatus { + /// The group or the logical expression has not been explored. + Unexplored, + /// The group or the logical expression is currently being explored. + Exploring, + /// The group or the logical expression has been explored. + Explored, +} diff --git a/optd-core/src/cascades/memo.rs b/optd-core/src/cascades/memo.rs new file mode 100644 index 0000000..2f6b710 --- /dev/null +++ b/optd-core/src/cascades/memo.rs @@ -0,0 +1,70 @@ +//! Memo table interface for query optimization. +//! +//! The memo table is a core data structure that stores expressions and their logical equivalences +//! during query optimization. It serves two main purposes: +//! +//! - Avoiding redundant optimization by memoizing already explored expressions +//! - Grouping logically equivalent expressions together to enable rule-based optimization +//! + +use std::sync::Arc; + +use super::{ + expressions::{LogicalExpression, LogicalExpressionId, ScalarExpression, ScalarExpressionId}, + groups::{RelationalGroupId, ScalarGroupId}, +}; +use anyhow::Result; + +#[trait_variant::make(Send)] +pub trait Memoize: Send + Sync + 'static { + /// Gets all logical expressions in a group. + async fn get_all_logical_exprs_in_group( + &self, + group_id: RelationalGroupId, + ) -> Result)>>; + + /// Adds a logical expression to an existing group. + /// Returns the group id of new group if merge happened. + async fn add_logical_expr_to_group( + &self, + logical_expr: &LogicalExpression, + group_id: RelationalGroupId, + ) -> Result; + + /// Adds a logical expression to the memo table. + /// Returns the group id of group if already exists, otherwise creates a new group. + async fn add_logical_expr(&self, logical_expr: &LogicalExpression) + -> Result; + + /// Gets all scalar expressions in a group. + async fn get_all_scalar_exprs_in_group( + &self, + group_id: ScalarGroupId, + ) -> Result)>>; + + /// Adds a scalar expression to an existing group. + /// Returns the group id of new group if merge happened. + async fn add_scalar_expr_to_group( + &self, + scalar_expr: &ScalarExpression, + group_id: ScalarGroupId, + ) -> Result; + + /// Adds a scalar expression to the memo table. + /// Returns the group id of group if already exists, otherwise creates a new group. + async fn add_scalar_expr(&self, scalar_expr: &ScalarExpression) -> Result; + + /// Merges two relational groups and returns the new group id. + async fn merge_relation_group( + &self, + from: RelationalGroupId, + to: RelationalGroupId, + ) -> Result; + + /// Merges two scalar groups and returns the new group id. + async fn merge_scalar_group( + &self, + from: ScalarGroupId, + to: ScalarGroupId, + ) -> Result; +} diff --git a/optd-core/src/cascades/mod.rs b/optd-core/src/cascades/mod.rs new file mode 100644 index 0000000..984d487 --- /dev/null +++ b/optd-core/src/cascades/mod.rs @@ -0,0 +1,172 @@ +use std::sync::Arc; + +use async_recursion::async_recursion; +use expressions::{LogicalExpression, ScalarExpression}; +use groups::{RelationalGroupId, ScalarGroupId}; +use memo::Memoize; + +use crate::{ + operators::{ + relational::logical::{filter::Filter, join::Join, scan::Scan, LogicalOperator}, + scalar::{add::Add, equal::Equal, ScalarOperator}, + }, + plans::{logical::PartialLogicalPlan, scalar::PartialScalarPlan}, +}; + +pub mod expressions; +pub mod groups; +pub mod memo; + +#[async_recursion] +pub async fn ingest_partial_logical_plan( + memo: &impl Memoize, + partial_logical_plan: &PartialLogicalPlan, +) -> anyhow::Result { + match partial_logical_plan { + PartialLogicalPlan::PartialMaterialized { operator } => { + let mut children_relations = Vec::new(); + for child in operator.children_relations().iter() { + children_relations.push(ingest_partial_logical_plan(memo, child).await?); + } + + let mut children_scalars = Vec::new(); + for child in operator.children_scalars().iter() { + children_scalars.push(ingest_partial_scalar_plan(memo, child).await?); + } + + memo.add_logical_expr(&operator.into_expr(&children_relations, &children_scalars)) + .await + } + + PartialLogicalPlan::UnMaterialized(group_id) => Ok(*group_id), + } +} + +#[async_recursion] +pub async fn ingest_partial_scalar_plan( + memo: &impl Memoize, + partial_scalar_plan: &PartialScalarPlan, +) -> anyhow::Result { + match partial_scalar_plan { + PartialScalarPlan::PartialMaterialized { operator } => { + let mut children = Vec::new(); + for child in operator.children_scalars().iter() { + children.push(ingest_partial_scalar_plan(memo, child).await?); + } + + memo.add_scalar_expr(&operator.into_expr(&children)).await + } + + PartialScalarPlan::UnMaterialized(group_id) => { + return Ok(*group_id); + } + } +} + +#[async_recursion] +async fn match_any_partial_logical_plan( + memo: &impl Memoize, + group: RelationalGroupId, +) -> anyhow::Result> { + let logical_exprs = memo.get_all_logical_exprs_in_group(group).await?; + let last_logical_expr = logical_exprs.last().unwrap().1.clone(); + + match last_logical_expr.as_ref() { + LogicalExpression::Scan(scan) => { + let predicate = match_any_partial_scalar_plan(memo, scan.predicate).await?; + Ok(Arc::new(PartialLogicalPlan::PartialMaterialized { + operator: LogicalOperator::Scan(Scan { + predicate, + table_name: scan.table_name.clone(), + }), + })) + } + LogicalExpression::Filter(filter) => { + let child = match_any_partial_logical_plan(memo, filter.child).await?; + let predicate = match_any_partial_scalar_plan(memo, filter.predicate).await?; + Ok(Arc::new(PartialLogicalPlan::PartialMaterialized { + operator: LogicalOperator::Filter(Filter { child, predicate }), + })) + } + LogicalExpression::Join(join) => { + let left = match_any_partial_logical_plan(memo, join.left).await?; + let right = match_any_partial_logical_plan(memo, join.right).await?; + let condition = match_any_partial_scalar_plan(memo, join.condition).await?; + Ok(Arc::new(PartialLogicalPlan::PartialMaterialized { + operator: LogicalOperator::Join(Join { + left, + right, + condition, + join_type: join.join_type.clone(), + }), + })) + } + } +} + +#[async_recursion] +async fn match_any_partial_scalar_plan( + memo: &impl Memoize, + group: ScalarGroupId, +) -> anyhow::Result> { + let scalar_exprs = memo.get_all_scalar_exprs_in_group(group).await?; + let last_scalar_expr = scalar_exprs.last().unwrap().1.clone(); + match last_scalar_expr.as_ref() { + ScalarExpression::Constant(constant) => { + Ok(Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::Constant(constant.clone()), + })) + } + ScalarExpression::ColumnRef(column_ref) => { + Ok(Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::ColumnRef(column_ref.clone()), + })) + } + ScalarExpression::Add(add) => { + let left = match_any_partial_scalar_plan(memo, add.left).await?; + let right = match_any_partial_scalar_plan(memo, add.right).await?; + Ok(Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::Add(Add { left, right }), + })) + } + ScalarExpression::Equal(equal) => { + let left = match_any_partial_scalar_plan(memo, equal.left).await?; + let right = match_any_partial_scalar_plan(memo, equal.right).await?; + Ok(Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::Equal(Equal { left, right }), + })) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{storage::memo::SqliteMemo, test_utils::*}; + use anyhow::Ok; + + #[tokio::test] + async fn test_ingest_partial_logical_plan() -> anyhow::Result<()> { + let memo = SqliteMemo::new("sqlite://memo.db").await?; + // select * from t1, t2 where t1.id = t2.id and t2.name = 'Memo' and t2.v1 = 1 + 1 + let partial_logical_plan = filter( + join( + "inner", + scan("t1", boolean(true)), + scan("t2", equal(column_ref(1), add(int64(1), int64(1)))), + equal(column_ref(1), column_ref(2)), + ), + equal(column_ref(2), string("Memo")), + ); + + let group_id = ingest_partial_logical_plan(&memo, &partial_logical_plan).await?; + let group_id_2 = ingest_partial_logical_plan(&memo, &partial_logical_plan).await?; + assert_eq!(group_id, group_id_2); + + // The plan should be the same, there is only one expression per group. + let result: Arc = + match_any_partial_logical_plan(&memo, group_id).await?; + assert_eq!(result, partial_logical_plan); + Ok(()) + } +} diff --git a/optd-core/src/engine/actions/analyzers/logical.rs b/optd-core/src/engine/actions/analyzers/logical.rs new file mode 100644 index 0000000..bdfe270 --- /dev/null +++ b/optd-core/src/engine/actions/analyzers/logical.rs @@ -0,0 +1,32 @@ +//! Analyzers for logical plans. +//! +//! Logical analyzers can compose with both logical and scalar analyzers +//! to extract information from logical plans into user-defined types. + +use super::scalar::ScalarAnalyzer; +use crate::{ + engine::{ + actions::{ + transformers::{logical::LogicalTransformer, scalar::ScalarTransformer}, + Action, Match as GenericMatch, + }, + patterns::logical::LogicalPattern, + }, + values::OptdExpr, +}; +use std::{cell::RefCell, rc::Rc}; + +/// Types of analyzers that can be composed in logical analysis. +#[derive(Clone, Debug)] +pub enum Composition { + LogicalAnalyzer(Rc>), + ScalarAnalyzer(Rc>), + ScalarTransformer(Rc>), + LogicalTransformer(Rc>), +} + +/// A single logical analyzer match. +pub type Match = GenericMatch; + +/// An analyzer for logical plans that produces user-defined types. +pub type LogicalAnalyzer = Action; diff --git a/optd-core/src/engine/actions/analyzers/mod.rs b/optd-core/src/engine/actions/analyzers/mod.rs new file mode 100644 index 0000000..a87885a --- /dev/null +++ b/optd-core/src/engine/actions/analyzers/mod.rs @@ -0,0 +1,10 @@ +//! Analyzer definitions for extracting information from plans and expressions. +//! +//! There are two types of analyzers: +//! - Logical analyzers: analyze logical plans +//! - Scalar analyzers: analyze scalar expressions +//! +//! Both use pattern matching and composition to produce an `OptdValue`. + +pub mod logical; +pub mod scalar; diff --git a/optd-core/src/engine/actions/analyzers/scalar.rs b/optd-core/src/engine/actions/analyzers/scalar.rs new file mode 100644 index 0000000..6676f57 --- /dev/null +++ b/optd-core/src/engine/actions/analyzers/scalar.rs @@ -0,0 +1,29 @@ +//! Analyzers for scalar expressions. +//! +//! Scalar analyzers can only compose with other scalar analyzers +//! to extract information from scalar expressions into user-defined types. + +use crate::{ + engine::{ + actions::{transformers::scalar::ScalarTransformer, Action, Match as GenericMatch}, + patterns::scalar::ScalarPattern, + }, + values::OptdExpr, +}; +use std::{cell::RefCell, rc::Rc}; + +/// Types of analyzers that can be composed in scalar analysis. +/// +/// Scalar analyzers can only compose with other scalar analyzers +/// and transformers. +#[derive(Clone, Debug)] +pub enum Composition { + ScalarAnalyzer(Rc>), + ScalarTransformer(Rc>), +} + +/// A single scalar analyzer match. +pub type Match = GenericMatch; + +/// An analyzer for scalar expressions that produces user-defined types. +pub type ScalarAnalyzer = Action; diff --git a/optd-core/src/engine/actions/mod.rs b/optd-core/src/engine/actions/mod.rs new file mode 100644 index 0000000..667787c --- /dev/null +++ b/optd-core/src/engine/actions/mod.rs @@ -0,0 +1,51 @@ +//! Actions for the OPTD optimizer's execution engine. +//! +//! This module provides a generic framework for pattern-matching based actions in the optimizer. +//! Actions include both analysis (extracting information) and transformation (rewriting plans). +//! +//! The framework supports: +//! - Pattern matching against plan structure +//! - Rule composition through intermediate bindings +//! - OPTD expression inputs + +pub mod analyzers; // Analysis actions that produce user-defined types +pub mod transformers; // Transformation actions that produce new plans + +use crate::values::OptdExpr; + +/// An action that matches patterns and produces outputs. +#[derive(Clone, Debug)] +pub struct Action { + /// Name identifying this action + pub name: String, + /// Input expressions for this action + pub inputs: Vec>, + /// Sequence of matches to try in order + pub matches: Vec, +} + +/// A single pattern match attempt within an action. +/// +/// # Type Parameters +/// - `Pattern`: The type of pattern to match against (logical/scalar) +/// - `Composition`: The type of sub-actions that can be composed +/// - `Output`: The type produced by this match (analysis result/new plan) +#[derive(Clone, Debug)] +pub struct Match { + /// Pattern to match against the input + pub pattern: Pattern, + /// Sequence of composed actions with their results bound to names + pub composition: Vec>, + /// Expression producing the final output + pub output: Output, +} + +/// A binding for an action's result to a name. +/// +/// Used in rule composition to chain multiple actions together. The bound +/// results can be referenced by name in subsequent actions or in the final +/// output expression. +/// +/// # Type Parameter +/// - `T`: Type of action being bound (analyzer/transformer) +type BindAs = (String, T); diff --git a/optd-core/src/engine/actions/transformers/logical.rs b/optd-core/src/engine/actions/transformers/logical.rs new file mode 100644 index 0000000..02402e5 --- /dev/null +++ b/optd-core/src/engine/actions/transformers/logical.rs @@ -0,0 +1,38 @@ +//! Logical plan transformations. +//! +//! Transforms logical plans through pattern matching and rule composition. +//! Can compose with both logical and scalar rules, as well as analyzers. + +use super::scalar::ScalarTransformer; +use crate::{ + engine::{ + actions::{ + analyzers::{logical::LogicalAnalyzer, scalar::ScalarAnalyzer}, + Action, Match as GenericMatch, + }, + patterns::scalar::ScalarPattern, + }, + plans::logical::PartialLogicalPlanExpr, +}; +use std::{cell::RefCell, rc::Rc}; + +/// Types of rules that can be composed in a logical transformation. +/// +/// Logical transformers can use: +/// - Other logical transformers +/// - Scalar transformers +/// - Logical analyzers +/// - Scalar analyzers +#[derive(Clone, Debug)] +pub enum Composition { + ScalarTransformer(Rc>), + ScalarAnalyzer(Rc>), + LogicalTransformer(Rc>), + LogicalAnalyzer(Rc>), +} + +/// A single logical transformer match. +pub type Match = GenericMatch; + +/// A transformer for logical plans that produces new logical plans. +pub type LogicalTransformer = Action; diff --git a/optd-core/src/engine/actions/transformers/mod.rs b/optd-core/src/engine/actions/transformers/mod.rs new file mode 100644 index 0000000..698af10 --- /dev/null +++ b/optd-core/src/engine/actions/transformers/mod.rs @@ -0,0 +1,11 @@ +//! Transformation rules for the OPTD optimizer. +//! +//! Provides two categories of transformations: +//! - Logical transformations: operate on logical plans +//! - Scalar transformations: operate on scalar expressions +//! +//! Each transformer uses pattern matching and rule composition to +//! produce new plans during optimization. + +pub mod logical; +pub mod scalar; diff --git a/optd-core/src/engine/actions/transformers/scalar.rs b/optd-core/src/engine/actions/transformers/scalar.rs new file mode 100644 index 0000000..bf9331b --- /dev/null +++ b/optd-core/src/engine/actions/transformers/scalar.rs @@ -0,0 +1,30 @@ +//! Scalar expression transformations. +//! +//! Transforms scalar expressions through pattern matching and rule composition. +//! Can compose with scalar rules and analyzers only. + +use crate::{ + engine::{ + actions::{analyzers::scalar::ScalarAnalyzer, Action, Match as GenericMatch}, + patterns::scalar::ScalarPattern, + }, + plans::scalar::PartialScalarPlanExpr, +}; +use std::{cell::RefCell, rc::Rc}; + +/// Types of rules that can be composed in a scalar transformation. +/// +/// Scalar transformers can only use: +/// - Other scalar transformers +/// - Scalar analyzers +#[derive(Clone, Debug)] +pub enum Composition { + ScalarTransformer(Rc>), + ScalarAnalyzer(Rc>), +} + +/// A single scalar transformer match. +pub type Match = GenericMatch; + +/// A transformer for scalar expressions that produces new scalar expressions. +pub type ScalarTransformer = Action; diff --git a/optd-core/src/engine/interpreter/analyzers/interpreter.rs b/optd-core/src/engine/interpreter/analyzers/interpreter.rs new file mode 100644 index 0000000..66973b9 --- /dev/null +++ b/optd-core/src/engine/interpreter/analyzers/interpreter.rs @@ -0,0 +1,101 @@ +// PartialLogicalPlan + Transformation IR => PartialLogicalPlan + +use std::collections::HashMap; + +use crate::{ + engine::patterns::{scalar::ScalarPattern, value::ValuePattern}, + plans::scalar::PartialScalarPlan, + values::OptdValue, +}; + +use super::scalar::ScalarAnalyzer; + +// TODO(Alexis): it is totally fair for analyzers to have transformer compostions actually. just their return type should differ. +// This is much more powerful. No reason not to do it. +pub struct Context { + pub value_bindings: HashMap, + pub scalar_bindings: HashMap, +} + +pub fn scalar_analyze( + plan: PartialScalarPlan, + transformer: &ScalarAnalyzer, +) -> anyhow::Result> { + for matcher in transformer.matches.iter() { + let mut context = Context { + value_bindings: HashMap::new(), + scalar_bindings: HashMap::new(), + }; + if match_scalar(&plan, &matcher.pattern, &mut context)? { + for (name, comp) in matcher.composition.iter() { + let value = scalar_analyze(context.scalar_bindings[name].clone(), &comp.borrow())?; + let Some(value) = value else { + return Ok(None); + }; + context.value_bindings.insert(name.clone(), value); + } + + return Ok(Some(matcher.output.evaluate(&context.value_bindings))); + } + } + Ok(None) +} + +fn match_scalar( + plan: &PartialScalarPlan, + pattern: &ScalarPattern, + context: &mut Context, +) -> anyhow::Result { + match pattern { + ScalarPattern::Any => Ok(true), + ScalarPattern::Not(scalar_pattern) => { + let x = match_scalar(plan, scalar_pattern, context)?; + Ok(!x) + } + ScalarPattern::Bind(name, scalar_pattern) => { + context.scalar_bindings.insert(name.clone(), plan.clone()); + match_scalar(plan, scalar_pattern, context) + } + ScalarPattern::Operator { + op_type, + content, + scalar_children, + } => { + let PartialScalarPlan::PartialMaterialized { operator } = plan else { + return Ok(false); //TODO: Call memo!! + }; + + if operator.operator_kind() != *op_type { + return Ok(false); + } + + for (subpattern, subplan) in scalar_children + .iter() + .zip(operator.children_scalars().iter()) + { + if !match_scalar(subplan, subpattern, context)? { + return Ok(false); + } + } + + for (subpattern, value) in content.iter().zip(operator.values().iter()) { + if !match_value(value, subpattern, context) { + return Ok(false); + } + } + + Ok(true) + } + } +} + +fn match_value(value: &OptdValue, pattern: &ValuePattern, context: &mut Context) -> bool { + match pattern { + ValuePattern::Any => true, + ValuePattern::Bind(name, optd_expr) => { + context.value_bindings.insert(name.clone(), value.clone()); + match_value(value, optd_expr, context) + } + ValuePattern::Match { expr } => expr.evaluate(&context.value_bindings) == *value, + } +} diff --git a/optd-core/src/engine/interpreter/expressions/mod.rs b/optd-core/src/engine/interpreter/expressions/mod.rs new file mode 100644 index 0000000..0f7d3f5 --- /dev/null +++ b/optd-core/src/engine/interpreter/expressions/mod.rs @@ -0,0 +1,2 @@ +pub mod plans; +pub mod values; diff --git a/optd-core/src/engine/interpreter/expressions/plans.rs b/optd-core/src/engine/interpreter/expressions/plans.rs new file mode 100644 index 0000000..449cdf2 --- /dev/null +++ b/optd-core/src/engine/interpreter/expressions/plans.rs @@ -0,0 +1,30 @@ +use std::collections::HashMap; + +use crate::{plans::PartialPlanExpr, values::OptdValue}; + +/// Evaluates a PartialPlanExpr to an PartialPlan using provided bindings. +impl PartialPlanExpr { + pub fn evaluate( + &self, + plan_bindings: &HashMap, + value_bindings: &HashMap, + ) -> Plan { + match self { + PartialPlanExpr::Plan(plan) => plan.clone(), + + PartialPlanExpr::Ref(name) => plan_bindings.get(name).cloned().unwrap_or_else(|| { + panic!("Undefined reference: {}", name); + }), + + PartialPlanExpr::IfThenElse { + cond, + then, + otherwise, + } => match cond.evaluate(value_bindings) { + OptdValue::Bool(true) => then.evaluate(plan_bindings, value_bindings), + OptdValue::Bool(false) => otherwise.evaluate(plan_bindings, value_bindings), + _ => panic!("IfThenElse condition must be boolean"), + }, + } + } +} diff --git a/optd-core/src/engine/interpreter/expressions/values.rs b/optd-core/src/engine/interpreter/expressions/values.rs new file mode 100644 index 0000000..434f3d6 --- /dev/null +++ b/optd-core/src/engine/interpreter/expressions/values.rs @@ -0,0 +1,197 @@ +//! Interpreter for OPTD-DSL expressions. +//! Evaluates OptdExpr with given bindings to produce OptdValue values. + +use std::collections::HashMap; + +use crate::values::{OptdExpr, OptdValue}; + +/// Evaluates an OptdExpr to an OptdValue value using provided bindings. +impl OptdExpr { + pub fn evaluate(&self, bindings: &HashMap) -> OptdValue { + match self { + OptdExpr::Value(val) => val.clone(), + + OptdExpr::Ref(name) => bindings.get(name).cloned().unwrap_or_else(|| { + panic!("Undefined reference: {}", name); + }), + + OptdExpr::IfThenElse { + cond, + then, + otherwise, + } => match cond.evaluate(bindings) { + OptdValue::Bool(true) => then.evaluate(bindings), + OptdValue::Bool(false) => otherwise.evaluate(bindings), + _ => panic!("IfThenElse condition must be boolean"), + }, + + OptdExpr::Eq { left, right } => { + OptdValue::Bool(left.evaluate(bindings) == right.evaluate(bindings)) + } + + OptdExpr::Lt { left, right } => { + match (left.evaluate(bindings), right.evaluate(bindings)) { + (OptdValue::Int64(l), OptdValue::Int64(r)) => OptdValue::Bool(l < r), + _ => panic!("Lt requires integer operands"), + } + } + + OptdExpr::Gt { left, right } => { + match (left.evaluate(bindings), right.evaluate(bindings)) { + (OptdValue::Int64(l), OptdValue::Int64(r)) => OptdValue::Bool(l > r), + _ => panic!("Gt requires integer operands"), + } + } + + OptdExpr::Add { left, right } => { + // TODO(alexis): overflow checks + match (left.evaluate(bindings), right.evaluate(bindings)) { + (OptdValue::Int64(l), OptdValue::Int64(r)) => OptdValue::Int64(l + r), + _ => panic!("Add requires integer operands"), + } + } + + OptdExpr::Sub { left, right } => { + // TODO(alexis): underflow checks + match (left.evaluate(bindings), right.evaluate(bindings)) { + (OptdValue::Int64(l), OptdValue::Int64(r)) => OptdValue::Int64(l - r), + _ => panic!("Sub requires integer operands"), + } + } + + OptdExpr::Mul { left, right } => { + // TODO(alexis): overflow checks + match (left.evaluate(bindings), right.evaluate(bindings)) { + (OptdValue::Int64(l), OptdValue::Int64(r)) => OptdValue::Int64(l * r), + _ => panic!("Mul requires integer operands"), + } + } + + OptdExpr::Div { left, right } => { + // TODO(alexis): div by 0 checks + match (left.evaluate(bindings), right.evaluate(bindings)) { + (OptdValue::Int64(l), OptdValue::Int64(r)) => { + if r == 0 { + panic!("Division by zero"); + } + OptdValue::Int64(l / r) + } + _ => panic!("Div requires integer operands"), + } + } + + OptdExpr::And { left, right } => { + match (left.evaluate(bindings), right.evaluate(bindings)) { + (OptdValue::Bool(l), OptdValue::Bool(r)) => OptdValue::Bool(l && r), + _ => panic!("And requires boolean operands"), + } + } + + OptdExpr::Or { left, right } => { + match (left.evaluate(bindings), right.evaluate(bindings)) { + (OptdValue::Bool(l), OptdValue::Bool(r)) => OptdValue::Bool(l || r), + _ => panic!("Or requires boolean operands"), + } + } + + OptdExpr::Not(expr) => match expr.evaluate(bindings) { + OptdValue::Bool(b) => OptdValue::Bool(!b), + _ => panic!("Not requires boolean operand"), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Helper to create test bindings + fn test_bindings() -> HashMap { + let mut map = HashMap::new(); + map.insert("x".to_string(), OptdValue::Int64(5)); + map.insert("y".to_string(), OptdValue::Int64(3)); + map.insert("flag".to_string(), OptdValue::Bool(true)); + map + } + + #[test] + fn test_basic_values() { + let bindings = test_bindings(); + assert_eq!( + OptdExpr::Value(OptdValue::Int64(42)).evaluate(&bindings), + OptdValue::Int64(42) + ); + } + + #[test] + fn test_references() { + let bindings = test_bindings(); + assert_eq!( + OptdExpr::Ref("x".to_string()).evaluate(&bindings), + OptdValue::Int64(5) + ); + } + + #[test] + fn test_arithmetic() { + let bindings = test_bindings(); + + // Addition + assert_eq!( + OptdExpr::Add { + left: Box::new(OptdExpr::Ref("x".to_string())), + right: Box::new(OptdExpr::Ref("y".to_string())) + } + .evaluate(&bindings), + OptdValue::Int64(8) + ); + + // Multiplication + assert_eq!( + OptdExpr::Mul { + left: Box::new(OptdExpr::Ref("x".to_string())), + right: Box::new(OptdExpr::Ref("y".to_string())) + } + .evaluate(&bindings), + OptdValue::Int64(15) + ); + } + + #[test] + fn test_boolean_logic() { + let bindings = test_bindings(); + + // AND + assert_eq!( + OptdExpr::And { + left: Box::new(OptdExpr::Ref("flag".to_string())), + right: Box::new(OptdExpr::Value(OptdValue::Bool(true))) + } + .evaluate(&bindings), + OptdValue::Bool(true) + ); + + // NOT + assert_eq!( + OptdExpr::Not(Box::new(OptdExpr::Ref("flag".to_string()))).evaluate(&bindings), + OptdValue::Bool(false) + ); + } + + #[test] + fn test_conditionals() { + let bindings = test_bindings(); + + let expr = OptdExpr::IfThenElse { + cond: Box::new(OptdExpr::Gt { + left: Box::new(OptdExpr::Ref("x".to_string())), + right: Box::new(OptdExpr::Ref("y".to_string())), + }), + then: Box::new(OptdExpr::Value(OptdValue::Int64(1))), + otherwise: Box::new(OptdExpr::Value(OptdValue::Int64(0))), + }; + + assert_eq!(expr.evaluate(&bindings), OptdValue::Int64(1)); + } +} diff --git a/optd-core/src/engine/interpreter/mod.rs b/optd-core/src/engine/interpreter/mod.rs new file mode 100644 index 0000000..f2feffc --- /dev/null +++ b/optd-core/src/engine/interpreter/mod.rs @@ -0,0 +1 @@ +pub mod expressions; diff --git a/optd-core/src/engine/interpreter/transformers/interpreter.rs b/optd-core/src/engine/interpreter/transformers/interpreter.rs new file mode 100644 index 0000000..fa405e1 --- /dev/null +++ b/optd-core/src/engine/interpreter/transformers/interpreter.rs @@ -0,0 +1,120 @@ +// PartialLogicalPlan + Transformation IR => PartialLogicalPlan + +use std::collections::HashMap; + +use crate::{ + engine::patterns::{scalar::ScalarPattern, value::ValuePattern}, + plans::scalar::PartialScalarPlan, + values::OptdValue, +}; + +use super::scalar::{Composition, ScalarTransformer}; + +pub struct Context { + pub scalar_bindings: HashMap, + pub value_bindings: HashMap, +} + +pub fn scalar_transform( + plan: PartialScalarPlan, + transformer: &ScalarTransformer, +) -> anyhow::Result> { + for matcher in transformer.matches.iter() { + let mut context = Context { + scalar_bindings: HashMap::new(), + value_bindings: HashMap::new(), + }; + if match_scalar(&plan, &matcher.pattern, &mut context)? { + // Apply compositions + for (name, comp) in matcher.composition.iter() { + match comp { + Composition::ScalarTransformer(scalar_transformer) => { + let new_plan = scalar_transform( + context.scalar_bindings[name].clone(), + scalar_transformer, + )?; + let Some(new_plan) = new_plan else { + return Ok(None); + }; + context.scalar_bindings.insert(name.clone(), new_plan); + } + Composition::ScalarAnalyzer(scalar_analyzer) => { + let value = + scalar_analyze(context.scalar_bindings[name].clone(), scalar_analyzer)?; + let Some(value) = value else { + return Ok(None); + }; + context.value_bindings.insert(name.clone(), value); + } + } + } + + return Ok(Some( + matcher + .output + .evaluate(&context.scalar_bindings, &context.value_bindings), + )); + } + } + + Ok(None) +} + +fn match_scalar( + plan: &PartialScalarPlan, + pattern: &ScalarPattern, + context: &mut Context, +) -> anyhow::Result { + match pattern { + ScalarPattern::Any => Ok(true), + ScalarPattern::Not(scalar_pattern) => { + let x = match_scalar(plan, scalar_pattern, context)?; + Ok(!x) + } + ScalarPattern::Bind(name, scalar_pattern) => { + context.scalar_bindings.insert(name.clone(), plan.clone()); + match_scalar(plan, scalar_pattern, context) + } + ScalarPattern::Operator { + op_type, + content, + scalar_children, + } => { + let PartialScalarPlan::PartialMaterialized { operator } = plan else { + return Ok(false); //TODO: Call memo!! + }; + + if operator.operator_kind() != *op_type { + return Ok(false); + } + + for (subpattern, subplan) in scalar_children + .iter() + .zip(operator.children_scalars().iter()) + { + if !match_scalar(subplan, subpattern, context)? { + return Ok(false); + } + } + + for (subpattern, value) in content.iter().zip(operator.values().iter()) { + if !match_value(value, subpattern, context) { + return Ok(false); + } + } + + Ok(true) + } + } +} + +fn match_value(value: &OptdValue, pattern: &ValuePattern, context: &mut Context) -> bool { + match pattern { + ValuePattern::Any => true, + ValuePattern::Bind(name, optd_expr) => { + context.value_bindings.insert(name.clone(), value.clone()); + match_value(value, optd_expr, context) + } + ValuePattern::Match { expr } => expr.evaluate(&context.value_bindings) == *value, + } +} diff --git a/optd-core/src/engine/mod.rs b/optd-core/src/engine/mod.rs new file mode 100644 index 0000000..47467e5 --- /dev/null +++ b/optd-core/src/engine/mod.rs @@ -0,0 +1,40 @@ +//! Core optimization engine for OPTD. +//! +//! The engine provides two key mechanisms for plan optimization: +//! +//! # Pattern Matching +//! The pattern system enables matching against different aspects of plans: +//! - Logical patterns: Match logical operator trees +//! - Scalar patterns: Match scalar expressions +//! - Value patterns: Match metadata values +//! +//! Patterns support: +//! - Wildcards +//! - Recursive operator matching +//! - Binding subplans for reuse +//! +//! # Actions +//! Actions define what to do when patterns match. There are two types: +//! +//! ## Analyzers +//! Extract information from plans: +//! - LogicalAnalyzer: Analyzes logical plans +//! - ScalarAnalyzer: Analyzes scalar expressions +//! - Both produce `OptdValue` as output +//! +//! ## Transformers +//! Transform plans into new plans: +//! - LogicalTransformer: Transforms logical plans +//! - ScalarTransformer: Transforms scalar expressions +//! - Both produce new partial plans as output +//! +//! # Composition +//! Both analyzers and transformers support composition: +//! - Chain multiple actions together +//! - Bind intermediate results +//! - Use results in subsequent actions +//! - Build complex transformations from simple ones + +pub mod actions; // Analyzers and transformers +pub mod interpreter; // Interpreter implementation of the engine +pub mod patterns; // Pattern matching system diff --git a/optd-core/src/engine/patterns/logical.rs b/optd-core/src/engine/patterns/logical.rs new file mode 100644 index 0000000..56ab762 --- /dev/null +++ b/optd-core/src/engine/patterns/logical.rs @@ -0,0 +1,35 @@ +//! Pattern matching for logical operators. + +use super::{scalar::ScalarPattern, value::ValuePattern}; +use crate::operators::relational::logical::LogicalOperatorKind; + +/// A pattern for matching logical operators in a query plan. +/// +/// Supports matching against the operator type, its values, and both +/// logical and scalar children. Patterns can be composed to match +/// complex subtrees and bind matched components for reuse. +#[derive(Clone, Debug)] +pub enum LogicalPattern { + /// Matches any logical operator. + Any, + + /// Binds a matched pattern to a name. + Bind { + /// Name to bind the matched subtree to + binding: String, + /// Pattern to match + pattern: Box, + }, + + /// Matches a specific logical operator. + Operator { + /// Type of logical operator to match + op_type: LogicalOperatorKind, + /// Value patterns for operator content + content: Vec>, + /// Patterns for logical children + logical_children: Vec>, + /// Patterns for scalar children + scalar_children: Vec>, + }, +} diff --git a/optd-core/src/engine/patterns/mod.rs b/optd-core/src/engine/patterns/mod.rs new file mode 100644 index 0000000..ada423a --- /dev/null +++ b/optd-core/src/engine/patterns/mod.rs @@ -0,0 +1,14 @@ +//! Pattern matching system for the OPTD optimizer. +//! +//! This module provides three distinct but related pattern types that work together +//! to match against different parts of a query plan: +//! - LogicalPattern: Matches logical operators and their structure +//! - ScalarPattern: Matches scalar expressions and their structure +//! - ValuePattern: Matches OPTD values and their content +//! +//! The pattern system mirrors the structure of the plan IR while providing +//! additional matching capabilities like wildcards and bindings. + +pub mod logical; +pub mod scalar; +pub mod value; diff --git a/optd-core/src/engine/patterns/scalar.rs b/optd-core/src/engine/patterns/scalar.rs new file mode 100644 index 0000000..ae81324 --- /dev/null +++ b/optd-core/src/engine/patterns/scalar.rs @@ -0,0 +1,33 @@ +//! Pattern matching for scalar operators. + +use super::value::ValuePattern; +use crate::operators::scalar::ScalarOperatorKind; + +/// A pattern for matching scalar operators in a query plan. +/// +/// Supports matching against the operator type, its values, and scalar +/// children. Unlike logical patterns, scalar patterns can only match +/// scalar children. +#[derive(Clone, Debug)] +pub enum ScalarPattern { + /// Matches any scalar operator. + Any, + + /// Binds a matched pattern to a name. + Bind { + /// Name to bind the matched subtree to + binding: String, + /// Pattern to match + pattern: Box, + }, + + /// Matches a specific scalar operator. + Operator { + /// Type of scalar operator to match + op_type: ScalarOperatorKind, + /// Value patterns for operator content + content: Vec>, + /// Patterns for scalar children + scalar_children: Vec>, + }, +} diff --git a/optd-core/src/engine/patterns/value.rs b/optd-core/src/engine/patterns/value.rs new file mode 100644 index 0000000..ea8c63e --- /dev/null +++ b/optd-core/src/engine/patterns/value.rs @@ -0,0 +1,25 @@ +//! Pattern matching for operator values. + +use crate::values::OptdExpr; + +/// A pattern for matching operator metadata values. +/// +/// Used within operator patterns to match metadata fields. Type bindings +/// are always leaf nodes as they match atomic values. +#[derive(Clone, Debug)] +pub enum ValuePattern { + /// Matches any value. + Any, + + /// Binds a matched value to a name. + Bind { + /// Name to bind the matched value to + binding: String, + }, + + /// Matches using an OPTD expression. + Match { + /// Expression to evaluate + expr: Box, + }, +} diff --git a/optd-core/src/expression.rs b/optd-core/src/expression.rs deleted file mode 100644 index 808cd7c..0000000 --- a/optd-core/src/expression.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Types for logical and physical expressions in the optimizer. - -use crate::memo::{GroupId, ScalarGroupId}; -use crate::operator::relational::logical::LogicalOperator; -use crate::operator::relational::physical::PhysicalOperator; - -/// A logical expression in the memo table. -/// -/// References children using [`GroupId`]s for expression sharing and memoization. -pub type LogicalExpression = LogicalOperator; - -/// A physical expression in the memo table. -/// -/// Like [`LogicalExpression`] but with specific implementation strategies. -pub type PhysicalExpression = PhysicalOperator; diff --git a/optd-core/src/lib.rs b/optd-core/src/lib.rs index 9ab7f2c..494ff29 100644 --- a/optd-core/src/lib.rs +++ b/optd-core/src/lib.rs @@ -1,13 +1,10 @@ -//! TODO Add docs. We will likely want to add a `#![doc = include_str!("../README.md")]` here. +#[allow(dead_code)] +pub mod cascades; +pub mod engine; +pub mod operators; +pub mod plans; +pub mod storage; +pub mod values; -#![warn(missing_docs)] -#![warn(clippy::missing_docs_in_private_items)] -#![warn(clippy::missing_errors_doc)] -#![warn(clippy::missing_panics_doc)] -#![warn(clippy::missing_safety_doc)] - -pub mod expression; -pub mod memo; -pub mod operator; -pub mod plan; -pub mod rules; +#[cfg(test)] +pub(crate) mod test_utils; diff --git a/optd-core/src/memo.rs b/optd-core/src/memo.rs deleted file mode 100644 index a8ea185..0000000 --- a/optd-core/src/memo.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Memo table implementation for query optimization. -//! -//! The memo table is a core data structure that stores expressions and their logical equivalences -//! during query optimization. It serves two main purposes: -//! -//! - Avoiding redundant optimization by memoizing already explored expressions -//! - Grouping logically equivalent expressions together to enable rule-based optimization -//! -//! # Structure -//! -//! - Each unique expression is assigned an expression ID (either [`LogicalExpressionId`], -//! [`PhysicalExpressionId`], or [`ScalarExpressionId`]) -//! - Logically equivalent expressions are grouped together under a [`GroupId`] -//! - Logically equivalent scalar expressions are grouped toegether under a [`ScalarGroupId`] -//! -//! # Usage -//! -//! The memo table provides methods to: -//! - Add new expressions and get their IDs -//! - Add expressions to existing groups -//! - Retrieve expressions in a group -//! - Look up group membership of expressions -//! - Create new groups for expressions - -use crate::expression::LogicalExpression; - -/// A unique identifier for a logical expression in the memo table. -#[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct LogicalExpressionId(u64); - -/// A unique identifier for a physical expression in the memo table. -#[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PhysicalExpressionId(u64); - -/// A unique identifier for a scalar expression in the memo table. -#[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ScalarExpressionId(u64); - -/// A unique identifier for a group of relational expressions in the memo table. -#[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct GroupId(u64); - -/// A unique identifier for a group of scalar expressions in the memo table. -#[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ScalarGroupId(u64); - -/// TODO(alexis) Add fields & link to storage layer. -pub struct Memo; - -/// TODO(alexis) Stabilize API by first expanding the Python code. -impl Memo { - /// TODO(alexis) Add docs. - pub async fn add_logical_expr_to_group( - &mut self, - _group_id: GroupId, - _logical_expr: LogicalExpression, - ) -> LogicalExpressionId { - todo!() - } -} diff --git a/optd-core/src/operator/mod.rs b/optd-core/src/operator/mod.rs deleted file mode 100644 index dfe253b..0000000 --- a/optd-core/src/operator/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! This module contains type definitions related to query plan operators, both relational (logical -//! / physical) and scalar. - -pub mod relational; -pub mod scalar; diff --git a/optd-core/src/operator/relational/logical/filter.rs b/optd-core/src/operator/relational/logical/filter.rs deleted file mode 100644 index f2146d8..0000000 --- a/optd-core/src/operator/relational/logical/filter.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! A logical filter. - -/// Logical filter operator that selects rows matching a condition. -/// -/// Takes input relation (`Relation`) and filters rows using a boolean predicate (`Scalar`). -#[derive(Clone)] -pub struct Filter { - /// The input relation. - pub child: Relation, - /// The filter expression denoting the predicate condition for this filter operation. - /// - /// For example, a filter predicate could be `column_a > 42`, or it could be something like - /// `column_b < 100 AND column_c > 1000`. - pub predicate: Scalar, -} diff --git a/optd-core/src/operator/relational/logical/join.rs b/optd-core/src/operator/relational/logical/join.rs deleted file mode 100644 index 75d919a..0000000 --- a/optd-core/src/operator/relational/logical/join.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! A logical join. - -/// Logical join operator that combines rows from two relations. -/// -/// Takes left and right relations (`Relation`) and joins their rows using a join condition -/// (`Scalar`). -#[derive(Clone)] -pub struct Join { - /// TODO(alexis) Mocked for now. - pub join_type: String, - /// The left input relation. - pub left: Relation, - /// The right input relation. - pub right: Relation, - /// The join expression denoting the join condition that links the two input relations. - /// - /// For example, a join operation could have a condition on `t1.id = t2.id` (an equijoin). - pub condition: Scalar, -} diff --git a/optd-core/src/operator/relational/logical/mod.rs b/optd-core/src/operator/relational/logical/mod.rs deleted file mode 100644 index 0821b38..0000000 --- a/optd-core/src/operator/relational/logical/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Type definitions of logical operators in `optd`. - -pub mod filter; -pub mod join; -pub mod project; -pub mod scan; - -use filter::Filter; -use join::Join; -use project::Project; -use scan::Scan; - -/// Each variant of `LogicalOperator` represents a specific kind of logical operator. -/// -/// This type is generic over two types: -/// - `Relation`: Specifies whether the children relations are other logical operators or a group id. -/// - `Scalar`: Specifies whether the children scalars are other scalar operators or a group id. -/// -/// This makes it possible to reuse the `LogicalOperator` type in [`LogicalPlan`], -/// [`PartialLogicalPlan`], and [`LogicalExpression`]. -/// -/// [`LogicalPlan`]: crate::plan::logical_plan::LogicalPlan -/// [`PartialLogicalPlan`]: crate::plan::partial_logical_plan::PartialLogicalPlan -/// [`LogicalExpression`]: crate::expression::LogicalExpression -#[allow(missing_docs)] -#[derive(Clone)] -pub enum LogicalOperator { - Scan(Scan), - Filter(Filter), - Project(Project), - Join(Join), -} diff --git a/optd-core/src/operator/relational/logical/project.rs b/optd-core/src/operator/relational/logical/project.rs deleted file mode 100644 index 656e5f5..0000000 --- a/optd-core/src/operator/relational/logical/project.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! A logical projection. - -/// Logical project operator that specifies output columns. -/// -/// Takes input relation (`Relation`) and defines output columns/expressions -/// (`Scalar`). -#[derive(Clone)] -pub struct Project { - /// The input relation. - pub child: Relation, - /// TODO(everyone): What exactly is going on here? - pub fields: Vec, -} diff --git a/optd-core/src/operator/relational/logical/scan.rs b/optd-core/src/operator/relational/logical/scan.rs deleted file mode 100644 index 39dc625..0000000 --- a/optd-core/src/operator/relational/logical/scan.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! A logical scan. - -/// Logical scan operator that reads from a base table. -/// -/// Reads from table (`String`) and optionally filters rows using a pushdown predicate -/// (`Scalar`). -#[derive(Clone)] -pub struct Scan { - /// TODO(alexis) Mocked for now. - pub table_name: String, - /// An optional filter expression for predicate pushdown into scan operators. - /// - /// For example, a `Filter(Scan(A), column_a < 42)` can be converted into a predicate pushdown - /// `Scan(A, column < 42)` to prevent having to materialize many tuples. - pub predicate: Option, -} diff --git a/optd-core/src/operator/relational/physical/filter/filter.rs b/optd-core/src/operator/relational/physical/filter/filter.rs deleted file mode 100644 index 9af1df6..0000000 --- a/optd-core/src/operator/relational/physical/filter/filter.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Physical filter operator that applies a boolean predicate to filter input rows. -/// -/// Takes a child operator (`Relation`) providing input rows and a predicate expression -/// (`Scalar`) that evaluates to true/false. Only rows where predicate is true -/// are emitted. -#[derive(Clone)] -pub struct PhysicalFilter { - pub child: Relation, - pub predicate: Scalar, -} diff --git a/optd-core/src/operator/relational/physical/join/hash_join.rs b/optd-core/src/operator/relational/physical/join/hash_join.rs deleted file mode 100644 index 4f06d01..0000000 --- a/optd-core/src/operator/relational/physical/join/hash_join.rs +++ /dev/null @@ -1,14 +0,0 @@ -/// Hash-based join operator that matches rows based on equality conditions. -/// -/// Takes left and right input relations (`Relation`) and joins their rows using -/// a join condition (`Scalar`). Builds hash table from build side (right) -/// and probes with rows from probe side (left). -#[derive(Clone)] -pub struct HashJoin { - pub join_type: String, - /// Left relation that probes hash table. - pub probe_side: Relation, - /// Right relation used to build hash table. - pub build_side: Relation, - pub condition: Scalar, -} diff --git a/optd-core/src/operator/relational/physical/join/merge_join.rs b/optd-core/src/operator/relational/physical/join/merge_join.rs deleted file mode 100644 index c8c4035..0000000 --- a/optd-core/src/operator/relational/physical/join/merge_join.rs +++ /dev/null @@ -1,13 +0,0 @@ -/// Merge join operator that matches rows based on equality conditions. -/// -/// Takes sorted left and right relations (`Relation`) and joins their rows using -/// a join condition (`Scalar`). Both inputs must be sorted on join keys. -#[derive(Clone)] -pub struct MergeJoin { - pub join_type: String, - /// Left sorted relation. - pub left_sorted: Relation, - /// Right sorted relation. - pub right_sorted: Relation, - pub condition: Scalar, -} diff --git a/optd-core/src/operator/relational/physical/join/nested_loop_join.rs b/optd-core/src/operator/relational/physical/join/nested_loop_join.rs deleted file mode 100644 index f069e10..0000000 --- a/optd-core/src/operator/relational/physical/join/nested_loop_join.rs +++ /dev/null @@ -1,13 +0,0 @@ -/// Nested-loop join operator that matches rows based on a predicate. -/// -/// Takes outer and inner relations (`Relation`) and joins their rows using -/// a join condition (`Scalar`). Scans inner relation for each outer row. -#[derive(Clone)] -pub struct NestedLoopJoin { - pub join_type: String, - /// Outer relation. - pub outer: Relation, - /// Inner relation scanned for each outer row. - pub inner: Relation, - pub condition: Scalar, -} diff --git a/optd-core/src/operator/relational/physical/mod.rs b/optd-core/src/operator/relational/physical/mod.rs deleted file mode 100644 index 9704168..0000000 --- a/optd-core/src/operator/relational/physical/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Type definitions of physical operators in optd. - -// TODO(connor): -// The module structure here is somewhat questionable, as it has multiple physical operators that -// should really only have 1 implementor (filter and project). -// For now, we can hold off on documenting stuff here until that is stabilized. -#![allow(missing_docs)] - -pub mod filter; -pub mod join; -pub mod project; -pub mod scan; - -use filter::filter::PhysicalFilter; -use join::{hash_join::HashJoin, merge_join::MergeJoin, nested_loop_join::NestedLoopJoin}; -use project::project::Project; -use scan::table_scan::TableScan; - -/// Each variant of `PhysicalOperator` represents a specific kind of physical operator. -/// -/// This type is generic over two types: -/// - `Relation`: Specifies whether the children relations are other physical operators or a group -/// id. -/// - `Scalar`: Specifies whether the children scalars are other scalar operators or a group id. -/// -/// This makes it possible to reuse the `PhysicalOperator` type in [`PhysicalPlan`] -/// and [`PhysicalExpression`]. -/// -/// [`PhysicalPlan`]: crate::plan::physical_plan::PhysicalPlan -/// [`PhysicalExpression`]: crate::expression::PhysicalExpression -#[allow(missing_docs)] -#[derive(Clone)] -pub enum PhysicalOperator { - TableScan(TableScan), - Filter(PhysicalFilter), - Project(Project), - HashJoin(HashJoin), - NestedLoopJoin(NestedLoopJoin), - SortMergeJoin(MergeJoin), -} diff --git a/optd-core/src/operator/relational/physical/project/project.rs b/optd-core/src/operator/relational/physical/project/project.rs deleted file mode 100644 index 2943461..0000000 --- a/optd-core/src/operator/relational/physical/project/project.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// Column projection operator that transforms input rows. -/// -/// Takes input relation (`Relation`) and projects columns/expressions (`Scalar`) -/// to produce output rows with selected/computed fields. -#[derive(Clone)] -pub struct Project { - pub child: Relation, - pub fields: Vec, -} diff --git a/optd-core/src/operator/relational/physical/scan/mod.rs b/optd-core/src/operator/relational/physical/scan/mod.rs deleted file mode 100644 index 9070935..0000000 --- a/optd-core/src/operator/relational/physical/scan/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod table_scan; diff --git a/optd-core/src/operator/relational/physical/scan/table_scan.rs b/optd-core/src/operator/relational/physical/scan/table_scan.rs deleted file mode 100644 index a60e343..0000000 --- a/optd-core/src/operator/relational/physical/scan/table_scan.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// Table scan operator that reads rows from a base table -/// -/// Reads from table (`String`) and optionally filters rows using -/// a pushdown predicate (`Scalar`). -#[derive(Clone)] -pub struct TableScan { - pub table_name: String, // TODO(alexis): Mocked for now. - pub predicate: Option, -} diff --git a/optd-core/src/operator/scalar/add.rs b/optd-core/src/operator/scalar/add.rs deleted file mode 100644 index 2adf42f..0000000 --- a/optd-core/src/operator/scalar/add.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// Addition expression for scalar values. -#[derive(Clone)] -pub struct Add { - pub left: Scalar, - pub right: Scalar, -} diff --git a/optd-core/src/operator/scalar/column_ref.rs b/optd-core/src/operator/scalar/column_ref.rs deleted file mode 100644 index ac2ace4..0000000 --- a/optd-core/src/operator/scalar/column_ref.rs +++ /dev/null @@ -1,3 +0,0 @@ -/// Column reference using position index (e.g. #0 for first column) -#[derive(Clone)] -pub struct ColumnRef(()); // TODO(alexis): Mocked for now. diff --git a/optd-core/src/operator/scalar/constants.rs b/optd-core/src/operator/scalar/constants.rs deleted file mode 100644 index 3b12840..0000000 --- a/optd-core/src/operator/scalar/constants.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// Constants that can appear in scalar expressions. -#[derive(Clone)] -pub enum Constant { - /// String constant (e.g. "hello"). - String(String), - /// Integer constant (e.g. 42). - Integer(i64), - /// Floating point constant (e.g. 3.14). - Float(f64), - /// Boolean constant (e.g. true, false). - Boolean(bool), -} diff --git a/optd-core/src/operator/scalar/mod.rs b/optd-core/src/operator/scalar/mod.rs deleted file mode 100644 index edb4b16..0000000 --- a/optd-core/src/operator/scalar/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Type definitions for scalar operators. - -// For now, we can hold off on documenting stuff here until that is stabilized. -#![allow(missing_docs)] - -pub mod add; -pub mod column_ref; -pub mod constants; - -use add::Add; -use column_ref::ColumnRef; -use constants::Constant; - -/// Each variant of `ScalarOperator` represents a specific kind of scalar operator. -/// -/// This type is generic over one type: -/// - `Scalar`: Specifies whether the children scalars are other scalar operators or a group id. -/// -/// This makes it possible to reuse the `ScalarOperator` type in [`LogicalPlan`], -/// [`PhysicalPlan`], and [`PartialLogicalPlan`]. -/// -/// [`LogicalPlan`]: crate::plan::logical_plan::LogicalPlan -/// [`PhysicalPlan`]: crate::plan::physical_plan::PhysicalPlan -/// [`PartialLogicalPlan`]: crate::plan::partial_logical_plan::PartialLogicalPlan -#[derive(Clone)] -pub enum ScalarOperator { - Constant(Constant), - ColumnRef(ColumnRef), - Add(Add), -} diff --git a/optd-core/src/operators/mod.rs b/optd-core/src/operators/mod.rs new file mode 100644 index 0000000..7d8cf31 --- /dev/null +++ b/optd-core/src/operators/mod.rs @@ -0,0 +1,26 @@ +//! Core operator types for OPTD's low-level IR (Intermediate Representation). +//! +//! These generic operator types serve multiple purposes in the system: +//! +//! 1. Plan Representations: +//! a. Logical Plans: Complete query execution plans +//! b. Scalar Plans: Expression trees within logical plans +//! c. Partial Plans: Mixed materialization states during optimization +//! +//! 2. Materialization States: +//! a. Fully Materialized: Complete trees of concrete operators +//! b. Partially Materialized: Mix of concrete operators and group references +//! c. Unmaterialized: Pure group references +//! +//! 3. Pattern Matching: +//! Templates for matching against any of the above plan forms +//! +//! This unified representation handles the complete lifecycle of plans: +//! - Initial logical/scalar plan creation +//! - Optimization process with group references +//! - Pattern matching for transformations +//! +//! TODO(alexis): This *entire* module will be codegened from the OPTD-DSL. + +pub mod relational; +pub mod scalar; diff --git a/optd-core/src/operators/relational/logical/filter.rs b/optd-core/src/operators/relational/logical/filter.rs new file mode 100644 index 0000000..75ac8b5 --- /dev/null +++ b/optd-core/src/operators/relational/logical/filter.rs @@ -0,0 +1,29 @@ +//! A logical filter. + +use super::LogicalOperator; +use crate::values::OptdValue; +use serde::Deserialize; + +/// Logical filter operator that selects rows matching a condition. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Filter { + /// The input relation. + pub child: Relation, + /// The filter predicate condition. + pub predicate: Scalar, +} + +impl Filter { + /// Create a new filter operator. + pub fn new(child: Relation, predicate: Scalar) -> Self { + Self { child, predicate } + } +} + +/// Creates a filter logical operator. +pub fn filter( + child: Relation, + predicate: Scalar, +) -> LogicalOperator { + LogicalOperator::Filter(Filter::new(child, predicate)) +} diff --git a/optd-core/src/operators/relational/logical/join.rs b/optd-core/src/operators/relational/logical/join.rs new file mode 100644 index 0000000..d2dac73 --- /dev/null +++ b/optd-core/src/operators/relational/logical/join.rs @@ -0,0 +1,40 @@ +//! A logical join. + +use super::LogicalOperator; +use crate::values::OptdValue; +use serde::Deserialize; + +/// Logical join operator that combines rows from two relations. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Join { + /// The type of join (inner, left, right). + pub join_type: Value, + /// The left input relation. + pub left: Relation, + /// The right input relation. + pub right: Relation, + /// The join condition. + pub condition: Scalar, +} + +impl Join { + /// Create a new join operator. + pub fn new(join_type: &str, left: Relation, right: Relation, condition: Scalar) -> Self { + Self { + join_type: OptdValue::String(join_type.into()), + left, + right, + condition, + } + } +} + +/// Creates a join logical operator. +pub fn join( + join_type: &str, + left: Relation, + right: Relation, + condition: Scalar, +) -> LogicalOperator { + LogicalOperator::Join(Join::new(join_type, left, right, condition)) +} diff --git a/optd-core/src/operators/relational/logical/mod.rs b/optd-core/src/operators/relational/logical/mod.rs new file mode 100644 index 0000000..42e6c7f --- /dev/null +++ b/optd-core/src/operators/relational/logical/mod.rs @@ -0,0 +1,169 @@ +//! Type definitions of logical operators in `optd`. +//! +//! This module provides the core logical operator types and implementations for the query optimizer. +//! Logical operators represent the high-level operations in a query plan without specifying +//! physical execution details. + +pub mod filter; +pub mod join; +pub mod scan; + +use filter::Filter; +use join::Join; +use scan::Scan; +use serde::Deserialize; + +use crate::{ + cascades::{ + expressions::LogicalExpression, + groups::{RelationalGroupId, ScalarGroupId}, + }, + values::OptdValue, +}; + +/// Each variant of `LogicalOperator` represents a specific kind of logical operator. +/// +/// This type is generic over three types: +/// - `Value`: The type of values stored in the operator (e.g., for table names, join types) +/// - `Relation`: Specifies whether the children relations are other logical operators or a group id +/// - `Scalar`: Specifies whether the children scalars are other scalar operators or a group id +/// +/// This makes it possible to reuse the `LogicalOperator` type in different contexts: +/// - Pattern matching: Using logical operators for matching rule patterns +/// - Partially materialized plans: Using logical operators during optimization +/// - Fully materialized plans: Using logical operators in physical execution +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub enum LogicalOperator { + /// Table scan operator + Scan(Scan), + /// Filter operator + Filter(Filter), + /// Join operator + Join(Join), +} + +/// The kind of logical operator. +/// +/// This enum represents the different types of logical operators available +/// in the system, used for classification and pattern matching. +#[derive(Debug, Clone, PartialEq, sqlx::Type)] +pub enum LogicalOperatorKind { + /// Represents a table scan operation + Scan, + /// Represents a filter operation + Filter, + /// Represents a join operation + Join, +} + +impl LogicalOperator +where + Relation: Clone, + Scalar: Clone, +{ + /// Returns the kind of logical operator. + /// + /// This method identifies the variant of the logical operator + /// without exposing its internal structure. + pub fn operator_kind(&self) -> LogicalOperatorKind { + match self { + LogicalOperator::Scan(_) => LogicalOperatorKind::Scan, + LogicalOperator::Filter(_) => LogicalOperatorKind::Filter, + LogicalOperator::Join(_) => LogicalOperatorKind::Join, + } + } + + /// Returns a vector of values associated with this operator. + /// + /// Different operators store different kinds of values: + /// - Scan: table name + /// - Filter: no values + /// - Join: join type + pub fn values(&self) -> Vec { + match self { + LogicalOperator::Scan(scan) => vec![scan.table_name.clone()], + LogicalOperator::Filter(_) => vec![], + LogicalOperator::Join(join) => vec![join.join_type.clone()], + } + } + + /// Returns a vector of child relations for this operator. + /// + /// The number of children depends on the operator type: + /// - Scan: no children + /// - Filter: one child + /// - Join: two children (left and right) + pub fn children_relations(&self) -> Vec { + match self { + LogicalOperator::Scan(_) => vec![], + LogicalOperator::Filter(filter) => vec![filter.child.clone()], + LogicalOperator::Join(join) => vec![join.left.clone(), join.right.clone()], + } + } + + /// Returns a vector of scalar expressions associated with this operator. + /// + /// Each operator type has specific scalar expressions: + /// - Scan: predicate + /// - Filter: predicate + /// - Join: condition + pub fn children_scalars(&self) -> Vec { + match self { + LogicalOperator::Scan(scan) => vec![scan.predicate.clone()], + LogicalOperator::Filter(filter) => vec![filter.predicate.clone()], + LogicalOperator::Join(join) => vec![join.condition.clone()], + } + } + + /// Converts the operator into a logical expression with the given children. + /// + /// # Arguments + /// * `children_relations` - Vector of relation group IDs for child relations + /// * `children_scalars` - Vector of scalar group IDs for scalar expressions + /// + /// # Returns + /// Returns a new `LogicalExpression` representing this operator + /// + /// # Panics + /// Panics if the number of provided children doesn't match the operator's requirements + pub fn into_expr( + &self, + children_relations: &[RelationalGroupId], + children_scalars: &[ScalarGroupId], + ) -> LogicalExpression { + let rel_size = children_relations.len(); + let scalar_size = children_scalars.len(); + + match self { + LogicalOperator::Scan(scan) => { + assert_eq!(rel_size, 0, "Scan: wrong number of relations"); + assert_eq!(scalar_size, 1, "Scan: wrong number of scalars"); + + LogicalExpression::Scan(Scan { + table_name: scan.table_name.clone(), + predicate: children_scalars[0], + }) + } + LogicalOperator::Filter(_) => { + assert_eq!(rel_size, 1, "Filter: wrong number of relations"); + assert_eq!(scalar_size, 1, "Filter: wrong number of scalars"); + + LogicalExpression::Filter(Filter { + child: children_relations[0], + predicate: children_scalars[0], + }) + } + LogicalOperator::Join(join) => { + assert_eq!(rel_size, 2, "Join: wrong number of relations"); + assert_eq!(scalar_size, 1, "Join: wrong number of scalars"); + + LogicalExpression::Join(Join { + left: children_relations[0], + right: children_relations[1], + condition: children_scalars[0], + join_type: join.join_type.clone(), + }) + } + } + } +} diff --git a/optd-core/src/operators/relational/logical/scan.rs b/optd-core/src/operators/relational/logical/scan.rs new file mode 100644 index 0000000..8e23abd --- /dev/null +++ b/optd-core/src/operators/relational/logical/scan.rs @@ -0,0 +1,32 @@ +//! A logical scan. + +use super::LogicalOperator; +use crate::values::OptdValue; +use serde::Deserialize; + +/// Logical scan operator that reads from a base table. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Scan { + /// The name of the table to scan. + pub table_name: Value, + /// The pushdown predicate. + pub predicate: Scalar, +} + +impl Scan { + /// Create a new scan operator. + pub fn new(table_name: &str, predicate: Scalar) -> Self { + Self { + table_name: OptdValue::String(table_name.into()), + predicate, + } + } +} + +/// Creates a scan logical operator. +pub fn scan( + table_name: &str, + predicate: Scalar, +) -> LogicalOperator { + LogicalOperator::Scan(Scan::new(table_name, predicate)) +} diff --git a/optd-core/src/operator/relational/mod.rs b/optd-core/src/operators/relational/mod.rs similarity index 100% rename from optd-core/src/operator/relational/mod.rs rename to optd-core/src/operators/relational/mod.rs diff --git a/optd-core/src/operators/relational/physical/filter/filter.rs b/optd-core/src/operators/relational/physical/filter/filter.rs new file mode 100644 index 0000000..2deb22e --- /dev/null +++ b/optd-core/src/operators/relational/physical/filter/filter.rs @@ -0,0 +1,29 @@ +//! A physical filter operator. + +use serde::Deserialize; + +use crate::{operators::relational::physical::PhysicalOperator, values::OptdValue}; + +/// A physical operator that filters input rows based on a predicate. +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct PhysicalFilter { + /// The input relation. + pub child: Relation, + /// The filter predicate. + pub predicate: Scalar, +} + +impl PhysicalFilter { + /// Create a new filter operator. + pub fn new(child: Relation, predicate: Scalar) -> Self { + Self { child, predicate } + } +} + +/// Creates a filter physical operator. +pub fn filter( + child: Relation, + predicate: Scalar, +) -> PhysicalOperator { + PhysicalOperator::Filter(PhysicalFilter::new(child, predicate)) +} diff --git a/optd-core/src/operator/relational/physical/filter/mod.rs b/optd-core/src/operators/relational/physical/filter/mod.rs similarity index 100% rename from optd-core/src/operator/relational/physical/filter/mod.rs rename to optd-core/src/operators/relational/physical/filter/mod.rs diff --git a/optd-core/src/operators/relational/physical/join/hash_join.rs b/optd-core/src/operators/relational/physical/join/hash_join.rs new file mode 100644 index 0000000..2f6e89d --- /dev/null +++ b/optd-core/src/operators/relational/physical/join/hash_join.rs @@ -0,0 +1,44 @@ +//! A physical hash join operator. + +use crate::{operators::relational::physical::PhysicalOperator, values::OptdValue}; +use serde::Deserialize; + +/// A physical operator that performs a hash-based join. +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct HashJoin { + /// The type of join. + pub join_type: Value, + /// Left relation that probes hash table. + pub probe_side: Relation, + /// Right relation used to build hash table. + pub build_side: Relation, + /// The join condition. + pub condition: Scalar, +} + +impl HashJoin { + /// Create a new hash join operator. + pub fn new( + join_type: &str, + probe_side: Relation, + build_side: Relation, + condition: Scalar, + ) -> Self { + Self { + join_type: OptdValue::String(join_type.into()), + probe_side, + build_side, + condition, + } + } +} + +/// Creates a hash join physical operator. +pub fn hash_join( + join_type: &str, + probe_side: Relation, + build_side: Relation, + condition: Scalar, +) -> PhysicalOperator { + PhysicalOperator::HashJoin(HashJoin::new(join_type, probe_side, build_side, condition)) +} diff --git a/optd-core/src/operators/relational/physical/join/merge_join.rs b/optd-core/src/operators/relational/physical/join/merge_join.rs new file mode 100644 index 0000000..7f0699f --- /dev/null +++ b/optd-core/src/operators/relational/physical/join/merge_join.rs @@ -0,0 +1,49 @@ +//! A physical merge join operator. + +use crate::{operators::relational::physical::PhysicalOperator, values::OptdValue}; +use serde::Deserialize; + +/// A physical operator that performs a sort-merge join. +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct MergeJoin { + /// The type of join. + pub join_type: Value, + /// Left sorted relation. + pub left_sorted: Relation, + /// Right sorted relation. + pub right_sorted: Relation, + /// The join condition. + pub condition: Scalar, +} + +impl MergeJoin { + /// Create a new merge join operator. + pub fn new( + join_type: &str, + left_sorted: Relation, + right_sorted: Relation, + condition: Scalar, + ) -> Self { + Self { + join_type: OptdValue::String(join_type.into()), + left_sorted, + right_sorted, + condition, + } + } +} + +/// Creates a merge join physical operator. +pub fn merge_join( + join_type: &str, + left_sorted: Relation, + right_sorted: Relation, + condition: Scalar, +) -> PhysicalOperator { + PhysicalOperator::SortMergeJoin(MergeJoin::new( + join_type, + left_sorted, + right_sorted, + condition, + )) +} diff --git a/optd-core/src/operator/relational/physical/join/mod.rs b/optd-core/src/operators/relational/physical/join/mod.rs similarity index 100% rename from optd-core/src/operator/relational/physical/join/mod.rs rename to optd-core/src/operators/relational/physical/join/mod.rs diff --git a/optd-core/src/operators/relational/physical/join/nested_loop_join.rs b/optd-core/src/operators/relational/physical/join/nested_loop_join.rs new file mode 100644 index 0000000..ad7b0bb --- /dev/null +++ b/optd-core/src/operators/relational/physical/join/nested_loop_join.rs @@ -0,0 +1,39 @@ +//! A physical nested loop join operator. + +use crate::{operators::relational::physical::PhysicalOperator, values::OptdValue}; +use serde::Deserialize; + +/// A physical operator that performs a nested loop join. +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct NestedLoopJoin { + /// The type of join. + pub join_type: Value, + /// The outer relation. + pub outer: Relation, + /// The inner relation. + pub inner: Relation, + /// The join condition. + pub condition: Scalar, +} + +impl NestedLoopJoin { + /// Create a new nested loop join operator. + pub fn new(join_type: &str, outer: Relation, inner: Relation, condition: Scalar) -> Self { + Self { + join_type: OptdValue::String(join_type.into()), + outer, + inner, + condition, + } + } +} + +/// Creates a nested loop join physical operator. +pub fn nested_loop_join( + join_type: &str, + outer: Relation, + inner: Relation, + condition: Scalar, +) -> PhysicalOperator { + PhysicalOperator::NestedLoopJoin(NestedLoopJoin::new(join_type, outer, inner, condition)) +} diff --git a/optd-core/src/operators/relational/physical/mod.rs b/optd-core/src/operators/relational/physical/mod.rs new file mode 100644 index 0000000..21d3b46 --- /dev/null +++ b/optd-core/src/operators/relational/physical/mod.rs @@ -0,0 +1,169 @@ +//! Type definitions of physical operators in optd. +//! +//! This module provides the core physical operator types and implementations for the query optimizer. +//! Physical operators represent the concrete execution strategies for query operations. + +pub mod filter; +pub mod join; +pub mod scan; + +use crate::{ + cascades::{ + expressions::PhysicalExpression, + groups::{RelationalGroupId, ScalarGroupId}, + }, + values::OptdValue, +}; +use filter::filter::PhysicalFilter; +use join::{hash_join::HashJoin, merge_join::MergeJoin, nested_loop_join::NestedLoopJoin}; +use scan::table_scan::TableScan; + +/// Each variant of `PhysicalOperator` represents a specific kind of physical operator. +/// +/// This type is generic over three types: +/// - `Value`: The type of values stored in the operator (e.g., for table names, join types) +/// - `Relation`: Specifies whether the children relations are other physical operators or a group id +/// - `Scalar`: Specifies whether the children scalars are other scalar operators or a group id +/// +/// This makes it possible to reuse the `PhysicalOperator` type in different contexts: +/// - Pattern matching: Using physical operators for matching rule patterns +/// - Partially materialized plans: Using physical operators during optimization +/// - Fully materialized plans: Using physical operators in physical execution +#[derive(Clone)] +pub enum PhysicalOperator { + /// Table scan operator + TableScan(TableScan), + /// Filter operator + Filter(PhysicalFilter), + /// Hash join operator + HashJoin(HashJoin), + /// Nested loop join operator + NestedLoopJoin(NestedLoopJoin), + /// Sort-merge join operator + SortMergeJoin(MergeJoin), +} + +/// The kind of physical operator. +/// +/// This enum represents the different types of physical operators available +/// in the system, used for classification and pattern matching. +#[derive(Debug, Clone, PartialEq, sqlx::Type)] +pub enum PhysicalOperatorKind { + /// Represents a table scan operation + TableScan, + /// Represents a filter operation + Filter, + /// Represents a hash join operation + HashJoin, + /// Represents a nested loop join operation + NestedLoopJoin, + /// Represents a sort-merge join operation + SortMergeJoin, +} + +impl PhysicalOperator +where + Relation: Clone, + Scalar: Clone, +{ + /// Returns the kind of physical operator. + pub fn operator_kind(&self) -> PhysicalOperatorKind { + match self { + PhysicalOperator::TableScan(_) => PhysicalOperatorKind::TableScan, + PhysicalOperator::Filter(_) => PhysicalOperatorKind::Filter, + PhysicalOperator::HashJoin(_) => PhysicalOperatorKind::HashJoin, + PhysicalOperator::NestedLoopJoin(_) => PhysicalOperatorKind::NestedLoopJoin, + PhysicalOperator::SortMergeJoin(_) => PhysicalOperatorKind::SortMergeJoin, + } + } + + /// Returns a vector of child relations for this operator. + pub fn children_relations(&self) -> Vec { + match self { + PhysicalOperator::TableScan(_) => vec![], + PhysicalOperator::Filter(filter) => vec![filter.child.clone()], + PhysicalOperator::HashJoin(join) => { + vec![join.probe_side.clone(), join.build_side.clone()] + } + PhysicalOperator::NestedLoopJoin(join) => vec![join.outer.clone(), join.inner.clone()], + PhysicalOperator::SortMergeJoin(join) => { + vec![join.left_sorted.clone(), join.right_sorted.clone()] + } + } + } + + /// Returns a vector of scalar expressions associated with this operator. + pub fn children_scalars(&self) -> Vec { + match self { + PhysicalOperator::TableScan(scan) => vec![scan.predicate.clone()], + PhysicalOperator::Filter(filter) => vec![filter.predicate.clone()], + PhysicalOperator::HashJoin(join) => vec![join.condition.clone()], + PhysicalOperator::NestedLoopJoin(join) => vec![join.condition.clone()], + PhysicalOperator::SortMergeJoin(join) => vec![join.condition.clone()], + } + } + + /// Converts the operator into a physical expression with the given children. + pub fn into_expr( + &self, + children_relations: &[RelationalGroupId], + children_scalars: &[ScalarGroupId], + ) -> PhysicalExpression { + let rel_size = children_relations.len(); + let scalar_size = children_scalars.len(); + + match self { + PhysicalOperator::TableScan(scan) => { + assert_eq!(rel_size, 0, "TableScan: wrong number of relations"); + assert_eq!(scalar_size, 1, "TableScan: wrong number of scalars"); + + PhysicalExpression::TableScan(TableScan { + table_name: scan.table_name.clone(), + predicate: children_scalars[0], + }) + } + PhysicalOperator::Filter(_) => { + assert_eq!(rel_size, 1, "Filter: wrong number of relations"); + assert_eq!(scalar_size, 1, "Filter: wrong number of scalars"); + + PhysicalExpression::Filter(PhysicalFilter { + child: children_relations[0], + predicate: children_scalars[0], + }) + } + PhysicalOperator::HashJoin(join) => { + assert_eq!(rel_size, 2, "HashJoin: wrong number of relations"); + assert_eq!(scalar_size, 1, "HashJoin: wrong number of scalars"); + + PhysicalExpression::HashJoin(HashJoin { + join_type: join.join_type.clone(), + probe_side: children_relations[0], + build_side: children_relations[1], + condition: children_scalars[0], + }) + } + PhysicalOperator::NestedLoopJoin(join) => { + assert_eq!(rel_size, 2, "NestedLoopJoin: wrong number of relations"); + assert_eq!(scalar_size, 1, "NestedLoopJoin: wrong number of scalars"); + + PhysicalExpression::NestedLoopJoin(NestedLoopJoin { + join_type: join.join_type.clone(), + outer: children_relations[0], + inner: children_relations[1], + condition: children_scalars[0], + }) + } + PhysicalOperator::SortMergeJoin(join) => { + assert_eq!(rel_size, 2, "SortMergeJoin: wrong number of relations"); + assert_eq!(scalar_size, 1, "SortMergeJoin: wrong number of scalars"); + + PhysicalExpression::SortMergeJoin(MergeJoin { + join_type: join.join_type.clone(), + left_sorted: children_relations[0], + right_sorted: children_relations[1], + condition: children_scalars[0], + }) + } + } + } +} diff --git a/optd-core/src/operator/relational/physical/project/mod.rs b/optd-core/src/operators/relational/physical/scan/mod.rs similarity index 63% rename from optd-core/src/operator/relational/physical/project/mod.rs rename to optd-core/src/operators/relational/physical/scan/mod.rs index 0b38a75..af29cd5 100644 --- a/optd-core/src/operator/relational/physical/project/mod.rs +++ b/optd-core/src/operators/relational/physical/scan/mod.rs @@ -1,2 +1,2 @@ #[allow(clippy::module_inception)] -pub mod project; +pub mod table_scan; diff --git a/optd-core/src/operators/relational/physical/scan/table_scan.rs b/optd-core/src/operators/relational/physical/scan/table_scan.rs new file mode 100644 index 0000000..b1de502 --- /dev/null +++ b/optd-core/src/operators/relational/physical/scan/table_scan.rs @@ -0,0 +1,31 @@ +//! A physical table scan operator. + +use crate::{operators::relational::physical::PhysicalOperator, values::OptdValue}; +use serde::Deserialize; + +/// A physical operator that scans rows from a table. +#[derive(Clone, Debug, Deserialize)] +pub struct TableScan { + /// The name of the table to scan. + pub table_name: Value, + /// The pushdown predicate. + pub predicate: Scalar, +} + +impl TableScan { + /// Create a new table scan operator. + pub fn new(table_name: &str, predicate: Scalar) -> Self { + Self { + table_name: OptdValue::String(table_name.to_string()), + predicate, + } + } +} + +/// Creates a table scan physical operator. +pub fn table_scan( + table_name: &str, + predicate: Scalar, +) -> PhysicalOperator { + PhysicalOperator::TableScan(TableScan::new(table_name, predicate)) +} diff --git a/optd-core/src/operators/scalar/add.rs b/optd-core/src/operators/scalar/add.rs new file mode 100644 index 0000000..67f7f1d --- /dev/null +++ b/optd-core/src/operators/scalar/add.rs @@ -0,0 +1,25 @@ +//! A scalar addition operator. + +use crate::{operators::scalar::ScalarOperator, values::OptdValue}; +use serde::Deserialize; + +/// A scalar operator that adds two values. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Add { + /// The left operand. + pub left: Scalar, + /// The right operand. + pub right: Scalar, +} + +impl Add { + /// Create a new addition operator. + pub fn new(left: Scalar, right: Scalar) -> Self { + Self { left, right } + } +} + +/// Creates an addition scalar operator. +pub fn add(left: Scalar, right: Scalar) -> ScalarOperator { + ScalarOperator::Add(Add::new(left, right)) +} diff --git a/optd-core/src/operators/scalar/column_ref.rs b/optd-core/src/operators/scalar/column_ref.rs new file mode 100644 index 0000000..bf37d0d --- /dev/null +++ b/optd-core/src/operators/scalar/column_ref.rs @@ -0,0 +1,25 @@ +//! A scalar column reference operator. + +use crate::{operators::scalar::ScalarOperator, values::OptdValue}; +use serde::Deserialize; + +/// A scalar operator that references a column by index. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct ColumnRef { + /// The index of the referenced column. + pub column_index: Value, +} + +impl ColumnRef { + /// Create a new column reference. + pub fn new(column_index: i64) -> Self { + Self { + column_index: OptdValue::Int64(column_index), + } + } +} + +/// Creates a column reference scalar operator. +pub fn column_ref(column_index: i64) -> ScalarOperator { + ScalarOperator::ColumnRef(ColumnRef::new(column_index)) +} diff --git a/optd-core/src/operators/scalar/constants.rs b/optd-core/src/operators/scalar/constants.rs new file mode 100644 index 0000000..0445b82 --- /dev/null +++ b/optd-core/src/operators/scalar/constants.rs @@ -0,0 +1,23 @@ +//! A scalar constant operator. + +use crate::{operators::scalar::ScalarOperator, values::OptdValue}; +use serde::{Deserialize, Serialize}; + +/// A scalar operator representing a constant value. +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct Constant { + /// The constant value. + pub value: Value, +} + +impl Constant { + /// Create a new constant. + pub fn new(value: OptdValue) -> Self { + Self { value } + } +} + +/// Creates a constant scalar operator. +pub fn constant(value: OptdValue) -> ScalarOperator { + ScalarOperator::Constant(Constant::new(value)) +} diff --git a/optd-core/src/operators/scalar/equal.rs b/optd-core/src/operators/scalar/equal.rs new file mode 100644 index 0000000..04e2cb8 --- /dev/null +++ b/optd-core/src/operators/scalar/equal.rs @@ -0,0 +1,25 @@ +//! A scalar equality operator. + +use crate::{operators::scalar::ScalarOperator, values::OptdValue}; +use serde::Deserialize; + +/// A scalar operator that compares two values for equality. +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Equal { + /// The left operand. + pub left: Scalar, + /// The right operand. + pub right: Scalar, +} + +impl Equal { + /// Create a new equality operator. + pub fn new(left: Scalar, right: Scalar) -> Self { + Self { left, right } + } +} + +/// Creates an equality scalar operator. +pub fn equal(left: Scalar, right: Scalar) -> ScalarOperator { + ScalarOperator::Equal(Equal::new(left, right)) +} diff --git a/optd-core/src/operators/scalar/mod.rs b/optd-core/src/operators/scalar/mod.rs new file mode 100644 index 0000000..b27dd38 --- /dev/null +++ b/optd-core/src/operators/scalar/mod.rs @@ -0,0 +1,127 @@ +//! Type definitions for scalar operators in `optd`. +//! +//! This module provides the core scalar operator types and implementations for the query optimizer. +//! Scalar operators represent expressions and computations that operate on individual values +//! rather than relations. + +pub mod add; +pub mod column_ref; +pub mod constants; +pub mod equal; + +use crate::{ + cascades::{expressions::ScalarExpression, groups::ScalarGroupId}, + values::OptdValue, +}; +use add::Add; +use column_ref::ColumnRef; +use constants::Constant; +use equal::Equal; +use serde::Deserialize; + +/// Each variant of `ScalarOperator` represents a specific kind of scalar operator. +/// +/// This type is generic over two types: +/// - `Value`: The type of values stored in the operator (e.g., constants, column indices) +/// - `Scalar`: Specifies whether the children scalars are other scalar operators or a group id +/// +/// This makes it possible to reuse the `ScalarOperator` type in different contexts: +/// - Pattern matching: Using scalar operators for matching rule patterns +/// - Partially materialized plans: Using scalar operators during optimization +/// - Fully materialized plans: Using scalar operators in physical execution +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub enum ScalarOperator { + /// Constant value operator + Constant(Constant), + /// Column reference operator + ColumnRef(ColumnRef), + /// Addition operator + Add(Add), + /// Equality comparison operator + Equal(Equal), +} + +/// The kind of scalar operator. +/// +/// This enum represents the different types of scalar operators available +/// in the system, used for classification and pattern matching. +#[derive(Debug, Clone, PartialEq, sqlx::Type)] +pub enum ScalarOperatorKind { + /// Represents a constant value + Constant, + /// Represents a column reference + ColumnRef, + /// Represents an addition operation + Add, + /// Represents an equality comparison + Equal, +} + +impl ScalarOperator +where + Scalar: Clone, +{ + /// Returns the kind of scalar operator. + pub fn operator_kind(&self) -> ScalarOperatorKind { + match self { + ScalarOperator::Constant(_) => ScalarOperatorKind::Constant, + ScalarOperator::ColumnRef(_) => ScalarOperatorKind::ColumnRef, + ScalarOperator::Add(_) => ScalarOperatorKind::Add, + ScalarOperator::Equal(_) => ScalarOperatorKind::Equal, + } + } + + /// Returns a vector of values associated with this operator. + pub fn values(&self) -> Vec { + match self { + ScalarOperator::Constant(constant) => vec![constant.value.clone()], + ScalarOperator::ColumnRef(column_ref) => vec![column_ref.column_index.clone()], + ScalarOperator::Add(_) => vec![], + ScalarOperator::Equal(_) => vec![], + } + } + + /// Returns a vector of scalar expressions that are children of this operator. + pub fn children_scalars(&self) -> Vec { + match self { + ScalarOperator::Constant(_) => vec![], + ScalarOperator::ColumnRef(_) => vec![], + ScalarOperator::Add(add) => vec![add.left.clone(), add.right.clone()], + ScalarOperator::Equal(equal) => vec![equal.left.clone(), equal.right.clone()], + } + } + + /// Converts the operator into a scalar expression with the given children. + pub fn into_expr(&self, children_scalars: &[ScalarGroupId]) -> ScalarExpression { + let scalar_size = children_scalars.len(); + + match self { + ScalarOperator::Constant(constant) => { + assert_eq!(scalar_size, 0, "Constant: expected no children"); + ScalarExpression::Constant(Constant { + value: constant.value.clone(), + }) + } + ScalarOperator::ColumnRef(column_ref) => { + assert_eq!(scalar_size, 0, "ColumnRef: expected no children"); + ScalarExpression::ColumnRef(ColumnRef { + column_index: column_ref.column_index.clone(), + }) + } + ScalarOperator::Add(_) => { + assert_eq!(scalar_size, 2, "Add: expected 2 children"); + ScalarExpression::Add(Add { + left: children_scalars[0], + right: children_scalars[1], + }) + } + ScalarOperator::Equal(_) => { + assert_eq!(scalar_size, 2, "Equal: expected 2 children"); + ScalarExpression::Equal(Equal { + left: children_scalars[0], + right: children_scalars[1], + }) + } + } + } +} diff --git a/optd-core/src/plan/logical_plan.rs b/optd-core/src/plan/logical_plan.rs deleted file mode 100644 index 2a5aff8..0000000 --- a/optd-core/src/plan/logical_plan.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! This module contains the [`LogicalPlan`] type, which is the representation of a logical query -//! plan from SQL. -//! -//! See the documentation for [`LogicalPlan`] for more information. - -use super::scalar_plan::ScalarPlan; -use crate::operator::relational::logical::LogicalOperator; -use std::sync::Arc; - -/// A representation of a logical query plan DAG (directed acyclic graph). -/// -/// A logical plan consists of only (materialized) logical operators and scalars. -/// -/// The root of the plan DAG _cannot_ be a scalar operator (and thus for now can only be a logical -/// operator). -/// -/// TODO(connor): add more docs. -#[derive(Clone)] -pub struct LogicalPlan { - /// Represents the current logical operator that is the root of the current subplan. - /// - /// Note that the children of the operator are other plans, which means that this data structure - /// is an in-memory DAG (directed acyclic graph) of logical operators. - pub node: Arc>, -} diff --git a/optd-core/src/plan/mod.rs b/optd-core/src/plan/mod.rs deleted file mode 100644 index 07b3897..0000000 --- a/optd-core/src/plan/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Types and definitions for representing logical, physical and scalar plans, both fully -//! materialized and partially materialized. - -pub mod logical_plan; -pub mod partial_logical_plan; -pub mod physical_plan; -pub mod scalar_plan; diff --git a/optd-core/src/plan/partial_logical_plan.rs b/optd-core/src/plan/partial_logical_plan.rs deleted file mode 100644 index dbda1bd..0000000 --- a/optd-core/src/plan/partial_logical_plan.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! This module contains the [`PartialLogicalPlan`] type, which is the representation of a partially -//! materialized logical query plan that is a mix of materialized logical operators and -//! unmaterialized group ID references to memo table groups of expressions. -//! -//! See the documentation for [`PartialLogicalPlan`] for more information. - -use crate::memo::{GroupId, ScalarGroupId}; -use crate::operator::relational::logical::LogicalOperator; -use crate::operator::scalar::ScalarOperator; -use std::sync::Arc; - -/// A partially materialized logical query plan represented as a DAG (directed acyclic graph). -/// -/// While a [`LogicalPlan`] contains fully materialized operator nodes, a [`PartialLogicalPlan`] -/// can contain both materialized nodes and references to unmaterialized memo groups. This enables -/// efficient plan exploration and transformation during query optimization. -/// -/// # Structure -/// -/// - Nodes can be either materialized operators or group references. -/// - Relational nodes can have both relational and scalar children. -/// - Scalar nodes can only have scalar children. -/// - The root must be a relational operator. -/// -/// # Type Parameters -/// -/// The plan uses [`Relation`] and [`Scalar`] to represent its node connections, -/// allowing mixing of materialized nodes and group references. -/// -/// [`LogicalPlan`]: crate::plan::logical_plan::LogicalPlan -#[derive(Clone)] -pub struct PartialLogicalPlan { - /// Represents the current logical operator that is the root of the current partially - /// materialized subplan. - /// - /// Note that the children of the operator are either a [`Relation`] or a [`Scalar`], both of - /// which are defined in this module. See their documentation for more information. - pub node: Arc>, -} - -/// A link to a relational node in a [`PartialLogicalPlan`]. -/// -/// This link (which denotes what kind of relational children the operators of a -/// [`PartialLogicalPlan`] can have) can be either: -/// - A materialized logical operator node. -/// - A reference (identifier) to an unmaterialized memo group. -#[derive(Clone)] -pub enum Relation { - /// A materialized logical operator node. - Operator(Arc>), - /// A reference (identifier) to an unmaterialized memo group. - GroupId(GroupId), -} - -/// A link to a scalar node in a [`PartialLogicalPlan`]. -/// -/// This link (which denotes what kind of scalar children the operators of a [`PartialLogicalPlan`] -/// can have) can be either: -/// - A materialized scalar operator node. -/// - A reference to an unmaterialized memo group. -#[derive(Clone)] -pub enum Scalar { - /// A materialized scalar operator node. - Operator(Arc>), - /// A reference to an unmaterialized memo group. - ScalarGroupId(ScalarGroupId), -} diff --git a/optd-core/src/plan/physical_plan.rs b/optd-core/src/plan/physical_plan.rs deleted file mode 100644 index bcb73f5..0000000 --- a/optd-core/src/plan/physical_plan.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! This module contains the [`PhysicalPlan`] type, which is the representation of a physical -//! execution plan that can be sent to a query execution engine. -//! -//! See the documentation for [`PhysicalPlan`] for more information. - -use super::scalar_plan::ScalarPlan; -use crate::operator::relational::physical::PhysicalOperator; -use std::sync::Arc; - -/// A representation of a physical query plan DAG (directed acyclic graph). -/// -/// A physical plan consists of only physical operators and scalars. -/// -/// The root of the plan DAG _cannot_ be a scalar operator (and thus for now can only be a physical -/// operator). -/// -/// TODO(connor): add more docs. -#[derive(Clone)] -pub struct PhysicalPlan { - /// Represents the current physical operator that is the root of the current subplan. - /// - /// Note that the children of the operator are other plans, which means that this data structure - /// is an in-memory DAG (directed acyclic graph) of physical operators. - pub node: Arc>, -} diff --git a/optd-core/src/plan/scalar_plan.rs b/optd-core/src/plan/scalar_plan.rs deleted file mode 100644 index f65c790..0000000 --- a/optd-core/src/plan/scalar_plan.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! TODO(everyone): Figure out what exactly a `ScalarPlan` is (tree? DAG? always materialized?) - -use crate::operator::scalar::ScalarOperator; -use std::sync::Arc; - -/// A representation of a scalar query plan DAG (directed acyclic graph). -#[derive(Clone)] -pub struct ScalarPlan { - /// Represents the current scalar operator that is the root of the current scalar subtree. - /// - /// TODO(connor): Figure out if scalar plans can be a DAG - pub node: Arc>, -} diff --git a/optd-core/src/plans/logical.rs b/optd-core/src/plans/logical.rs new file mode 100644 index 0000000..c4a41fc --- /dev/null +++ b/optd-core/src/plans/logical.rs @@ -0,0 +1,50 @@ +//! Logical plan representations for the OPTD optimizer. +//! +//! Provides three levels of plan materialization: +//! 1. Full materialization (LogicalPlan) +//! 2. Partial materialization (PartialLogicalPlan) +//! 3. Group references (LogicalGroupId) +//! +//! This allows the optimizer to work with plans at different stages +//! of materialization during the optimization process. + +use crate::{ + cascades::groups::RelationalGroupId, operators::relational::logical::LogicalOperator, + values::OptdValue, +}; + +use super::{ + scalar::{PartialScalarPlan, ScalarPlan}, + PartialPlanExpr, +}; +use std::sync::Arc; + +/// A fully materialized logical query plan. +/// +/// Contains a complete tree of logical operators where all children +/// (both logical and scalar) are fully materialized. Used for final +/// plan representation after optimization is complete. +#[derive(Clone, Debug, PartialEq)] +pub struct LogicalPlan { + operator: LogicalOperator, Arc>, +} + +/// A logical plan with varying levels of materialization. +/// +/// During optimization, plans can be in three states: +/// - Partially materialized: Single materialized operator with group references +/// - Unmaterialized: Pure group reference +#[derive(Clone, Debug, PartialEq)] +pub enum PartialLogicalPlan { + /// Single materialized operator with potentially unmaterialized children + PartialMaterialized { + operator: LogicalOperator, Arc>, + }, + + /// Reference to an optimization group containing equivalent plans + UnMaterialized(RelationalGroupId), +} + +/// Type alias for expressions that construct logical plans. +/// See PartialPlanExpr for the available expression constructs. +pub type PartialLogicalPlanExpr = PartialPlanExpr; diff --git a/optd-core/src/plans/mod.rs b/optd-core/src/plans/mod.rs new file mode 100644 index 0000000..adfa709 --- /dev/null +++ b/optd-core/src/plans/mod.rs @@ -0,0 +1,32 @@ +//! Plan expression system for constructing partial plans. +//! +//! Provides a generic expression type for building both logical and scalar +//! plans during optimization, with control flow and reference capabilities. + +use crate::values::OptdExpr; + +pub mod logical; +pub mod scalar; + +/// Expression type for constructing partial plans. +/// +/// Generic over the type of plan being constructed (logical or scalar) +/// to provide a unified expression system for both plan types. +#[derive(Clone, Debug)] +pub enum PartialPlanExpr { + /// Conditional plan construction + IfThenElse { + /// Condition (must be an OPTD expression) + cond: OptdExpr, + /// Plan to construct if condition is true + then: Box>, + /// Plan to construct if condition is false + otherwise: Box>, + }, + + /// Reference to a bound plan + Ref(String), + + /// Direct plan value + Plan(Plan), +} diff --git a/optd-core/src/plans/scalar.rs b/optd-core/src/plans/scalar.rs new file mode 100644 index 0000000..dbbed3c --- /dev/null +++ b/optd-core/src/plans/scalar.rs @@ -0,0 +1,46 @@ +//! Scalar expression representations for the OPTD optimizer. +//! +//! Provides three levels of scalar expression materialization: +//! 1. Full materialization (ScalarPlan) +//! 2. Partial materialization (PartialScalarPlan) +//! 3. Group references (ScalarGroupId) +//! +//! This allows the optimizer to work with expressions at different stages +//! of materialization during the optimization process. + +use crate::{ + cascades::groups::ScalarGroupId, operators::scalar::ScalarOperator, values::OptdValue, +}; + +use super::PartialPlanExpr; +use std::sync::Arc; + +/// A fully materialized scalar expression tree. +/// +/// Contains a complete tree of scalar operators where all children +/// are also fully materialized. Used for final expression representation +/// after optimization is complete. +#[derive(Clone, Debug, PartialEq)] +pub struct ScalarPlan { + operator: ScalarOperator>, +} + +/// A scalar expression with varying levels of materialization. +/// +/// During optimization, expressions can be in three states: +/// - Partially materialized: Single materialized operator with group references +/// - Unmaterialized: Pure group reference +#[derive(Clone, Debug, PartialEq)] +pub enum PartialScalarPlan { + /// Single materialized operator with potentially unmaterialized children + PartialMaterialized { + operator: ScalarOperator>, + }, + + /// Reference to an optimization group containing equivalent expressions + UnMaterialized(ScalarGroupId), +} + +/// Type alias for expressions that construct scalar plans. +/// See PartialPlanExpr for the available expression constructs. +pub type PartialScalarPlanExpr = PartialPlanExpr; diff --git a/optd-core/src/rules/implementation/hash_join.rs b/optd-core/src/rules/implementation/hash_join.rs deleted file mode 100644 index 9738be4..0000000 --- a/optd-core/src/rules/implementation/hash_join.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! The rule for implementing `Join` as a `HashJoin`. -//! -//! See [`HashJoinRule`] for more information. - -use super::*; -use crate::operator::relational::{ - logical::{join::Join, LogicalOperator}, - physical::{join::hash_join::HashJoin, PhysicalOperator}, -}; - -/// A unit / marker struct for implementing `HashJoin`. -/// -/// This implementation rule converts a logical `Join` into a physical `HashJoin` operator. -pub struct HashJoinRule; - -// TODO: rule may fail, need to check join condition -// https://github.com/cmu-db/optd/issues/15 -impl ImplementationRule for HashJoinRule { - fn check_and_apply(&self, expr: LogicalExpression) -> Option { - let LogicalOperator::Join(Join { - join_type, - left, - right, - condition, - }) = expr - else { - return None; - }; - - Some(PhysicalOperator::HashJoin(HashJoin { - join_type, - probe_side: left, - build_side: right, - condition, - })) - } -} diff --git a/optd-core/src/rules/implementation/mod.rs b/optd-core/src/rules/implementation/mod.rs deleted file mode 100644 index 9e461a1..0000000 --- a/optd-core/src/rules/implementation/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! This module contains the implementation rule trait / API, as well as the rules that implement -//! said trait. -//! -//! TODO(connor): Add more docs. - -use crate::expression::{LogicalExpression, PhysicalExpression}; - -/// The interface for implementation rules, which help convert logical plans into physical -/// (executable) query plans. -#[trait_variant::make(Send)] -#[allow(dead_code)] -pub trait ImplementationRule { - /// Checks if the given expression matches the given rule and applies the implementation. - /// - /// If the input expression does not match the current rule's pattern, then this method returns - /// `None`. Otherwise, it applies the implementation and returns the corresponding - /// [`PhysicalExpression`]. - /// - /// Implementation rules are defined to be a mapping from a logical operator to a physical - /// operator. This method is used by the rule engine to check if an implementation / algorithm - /// can be applied to a logical expression in the memo table. If so, it emits a new physical - /// expression, representing a executor in a query execution engine. - /// - /// One of the main differences between `ImplementationRule` and [`TransformationRule`] is that - /// transformation rules can create several new partially materialized logical plans, which - /// necessitates creating a partial logical plan binding (encoded as [`PartialLogicalPlan`]) - /// before actually applying the transformation. Additionally, the transformation rules have to - /// materialize child expressions of the input expression in order to match several layers of - /// operators in a logical plan, which implementation rules do not need. - /// - /// [`TransformationRule`]: crate::rules::transformation::TransformationRule - /// [`PartialLogicalPlan`]: crate::plan::partial_logical_plan::PartialLogicalPlan - fn check_and_apply(&self, expr: LogicalExpression) -> Option; -} - -pub mod hash_join; -pub mod physical_filter; -pub mod table_scan; diff --git a/optd-core/src/rules/implementation/physical_filter.rs b/optd-core/src/rules/implementation/physical_filter.rs deleted file mode 100644 index a169e24..0000000 --- a/optd-core/src/rules/implementation/physical_filter.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! The rule for implementing a logical `Filter` as a physical `Filter`. -//! -//! See [`PhysicalFilterRule`] for more information. - -use super::*; -use crate::operator::relational::{ - logical::{filter::Filter as LogicalFilter, LogicalOperator}, - physical::{filter::filter::PhysicalFilter, PhysicalOperator}, -}; - -/// A unit / marker struct for implementing `PhysicalFilterRule`. -/// -/// This mplementation rule converts a logical `Filter` into a physical `Filter` operator. -pub struct PhysicalFilterRule; - -impl ImplementationRule for PhysicalFilterRule { - fn check_and_apply(&self, expr: LogicalExpression) -> Option { - let LogicalOperator::Filter(LogicalFilter { child, predicate }) = expr else { - return None; - }; - - Some(PhysicalOperator::Filter(PhysicalFilter { - child, - predicate, - })) - } -} diff --git a/optd-core/src/rules/implementation/table_scan.rs b/optd-core/src/rules/implementation/table_scan.rs deleted file mode 100644 index 22382c2..0000000 --- a/optd-core/src/rules/implementation/table_scan.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! The rule for implementing `Scan` as a `TableScan`. -//! -//! See [`TableScanRule`] for more information. - -use crate::operator::relational::{ - logical::{scan::Scan, LogicalOperator}, - physical::{scan::table_scan::TableScan, PhysicalOperator}, -}; - -use super::*; - -/// A unit / marker struct for implementing `TableScan`. -/// -/// This implementation rule converts a logical `Scan` into a physical `TableScan` operator. -pub struct TableScanRule; - -impl ImplementationRule for TableScanRule { - fn check_and_apply(&self, expr: LogicalExpression) -> Option { - let LogicalOperator::Scan(Scan { - table_name, - predicate, - }) = expr - else { - return None; - }; - - Some(PhysicalOperator::TableScan(TableScan { - table_name, - predicate, - })) - } -} diff --git a/optd-core/src/rules/mod.rs b/optd-core/src/rules/mod.rs deleted file mode 100644 index b7e1002..0000000 --- a/optd-core/src/rules/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! This module contains all rules that the optimizer has available, including both transformation -//! and implementation rules. - -mod implementation; -mod transformation; diff --git a/optd-core/src/rules/transformation/join_associativity.rs b/optd-core/src/rules/transformation/join_associativity.rs deleted file mode 100644 index 2e48be0..0000000 --- a/optd-core/src/rules/transformation/join_associativity.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! The rule for join associativity. -//! -//! See [`JoinAssociativityRule`] for more information. - -use super::*; - -/// A unit / marker struct for join associativity. -/// -/// Since joining is an associative operation, we can convert a `Join(Join(A, B), C)` into a -/// `Join(A, Join(B, C))`. -pub struct JoinAssociativityRule; - -impl TransformationRule for JoinAssociativityRule { - async fn check_pattern( - &self, - _expr: LogicalExpression, - _memo: &Memo, - ) -> Vec { - todo!() - } - - fn apply(&self, _expr: PartialLogicalPlan) -> PartialLogicalPlan { - todo!() - } -} diff --git a/optd-core/src/rules/transformation/join_commutativity.rs b/optd-core/src/rules/transformation/join_commutativity.rs deleted file mode 100644 index 959d6db..0000000 --- a/optd-core/src/rules/transformation/join_commutativity.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! The rule for join commutativity. -//! -//! See [`JoinCommutativityRule`] for more information. - -use super::*; - -/// A unit / marker struct for join commutativity. -/// -/// Since joining is an commutative operation, we can convert a `Join(A, B)` into a `Join(B, C)`. -pub struct JoinCommutativityRule; - -impl TransformationRule for JoinCommutativityRule { - async fn check_pattern( - &self, - _expr: LogicalExpression, - _memo: &Memo, - ) -> Vec { - todo!() - } - - fn apply(&self, _expr: PartialLogicalPlan) -> PartialLogicalPlan { - todo!() - } -} diff --git a/optd-core/src/rules/transformation/mod.rs b/optd-core/src/rules/transformation/mod.rs deleted file mode 100644 index 3152e94..0000000 --- a/optd-core/src/rules/transformation/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! This module contains the transformation rule trait / API, as well as the rules that implement -//! said trait. -//! -//! TODO(everyone) Add more docs. - -use crate::{ - expression::LogicalExpression, memo::Memo, plan::partial_logical_plan::PartialLogicalPlan, -}; - -/// The interface for transformation rules, which help enumerate logically equivalent plans during -/// the optimization search. -#[trait_variant::make(Send)] -#[allow(dead_code)] -pub trait TransformationRule { - /// Checks if the transformation rule matches the current expression and its childrenm, and - /// returns a vector of partially materialized logical plans. - /// - /// This returns a vector because the rule matching the input root expression could have matched - /// with multiple child expressions. - /// - /// For example, let's say the input expression is `Filter(G1)`, and the group G1 has two - /// expressions `e1 = Join(Join(A, B), C)` and `e2 = Join(A, Join(B, C))`. - /// - /// If the rule wants to match against `Filter(Join(?L, ?R))`, then this function will partially - /// materialize two expressions `Filter(e1)` and `Filter(e2)`. It is then up to the memo table - /// API to apply modifications to the partially materialized logical plans (for example, a - /// filter pushdown under a `Join`). - /// - /// TODO: Ideally this should return a `Stream` instead of a fully materialized Vector. - async fn check_pattern(&self, expr: LogicalExpression, memo: &Memo) -> Vec; - - /// Applies modifications to a partially materialized logical plan. - /// - /// These changes can create new logical or scalar expressions. However, note that - /// transformation rules will _not_ create new physical expressions. - /// - /// TODO(everyone) Figure out what the return type should really be. - fn apply(&self, expr: PartialLogicalPlan) -> PartialLogicalPlan; -} - -pub mod join_associativity; -pub mod join_commutativity; diff --git a/optd-core/src/storage/memo.rs b/optd-core/src/storage/memo.rs new file mode 100644 index 0000000..cbbd3c9 --- /dev/null +++ b/optd-core/src/storage/memo.rs @@ -0,0 +1,649 @@ +//! An implementation of the memo table using SQLite. + +use std::{str::FromStr, sync::Arc, time::Duration}; + +use super::transaction::Transaction; +use anyhow::Result; +use sqlx::{ + sqlite::{SqliteConnectOptions, SqliteJournalMode}, + SqliteConnection, SqlitePool, +}; + +use crate::cascades::{ + expressions::*, + groups::{ExplorationStatus, RelationalGroupId, ScalarGroupId}, + memo::Memoize, +}; +use crate::operators::relational::logical::LogicalOperatorKind; +use crate::operators::scalar::ScalarOperatorKind; + +/// A Storage manager that manages connections to the database. +pub struct SqliteMemo { + /// A async connection pool to the SQLite database. + db: SqlitePool, + /// SQL query string to get all logical expressions in a group. + get_all_logical_exprs_in_group_query: String, + /// SQL query string to get all scalar expressions in a group. + get_all_scalar_exprs_in_group_query: String, +} + +impl SqliteMemo { + /// Create a new storage manager that connects to the SQLite database at the given URL. + pub async fn new(database_url: &str) -> anyhow::Result { + let options = SqliteConnectOptions::from_str(database_url)? + .create_if_missing(true) + .journal_mode(SqliteJournalMode::Wal) + .busy_timeout(Duration::from_secs(30)); + Self::new_with_options(options).await + } + + /// Create a new storage manager backed by an in-memory SQLite database. + pub async fn new_in_memory() -> anyhow::Result { + let options = SqliteConnectOptions::from_str(":memory:")?; + Self::new_with_options(options).await + } + + /// Creates a new storage manager with the given options. + async fn new_with_options(options: SqliteConnectOptions) -> anyhow::Result { + let memo = Self { + db: SqlitePool::connect_with(options).await?, + get_all_logical_exprs_in_group_query: get_all_logical_exprs_in_group_query().into(), + get_all_scalar_exprs_in_group_query: get_all_scalar_exprs_in_group_query().into(), + }; + memo.migrate().await?; + Ok(memo) + } + + /// Runs pending migrations. + async fn migrate(&self) -> anyhow::Result<()> { + // sqlx::migrate! takes the path relative to the root of the crate. + sqlx::migrate!("src/storage/migrations") + .run(&self.db) + .await?; + Ok(()) + } + + /// Begin a new transaction. + pub(super) async fn begin(&self) -> anyhow::Result> { + let txn = self.db.begin().await?; + Transaction::new(txn).await + } +} + +impl Memoize for SqliteMemo { + async fn get_all_logical_exprs_in_group( + &self, + group_id: RelationalGroupId, + ) -> Result)>> { + #[derive(sqlx::FromRow)] + struct LogicalExprRecord { + logical_expression_id: LogicalExpressionId, + data: sqlx::types::Json>, + } + + let mut txn = self.begin().await?; + let representative_group_id = self.get_representative_group_id(&mut txn, group_id).await?; + let logical_exprs: Vec = + sqlx::query_as(&self.get_all_logical_exprs_in_group_query) + .bind(representative_group_id) + .fetch_all(&mut *txn) + .await?; + + txn.commit().await?; + Ok(logical_exprs + .into_iter() + .map(|record| (record.logical_expression_id, record.data.0)) + .collect()) + } + + async fn add_logical_expr_to_group( + &self, + logical_expr: &LogicalExpression, + group_id: RelationalGroupId, + ) -> Result { + let group_id = self + .add_logical_expr_to_group_inner(logical_expr, Some(group_id)) + .await?; + Ok(group_id) + } + + async fn add_logical_expr( + &self, + logical_expr: &LogicalExpression, + ) -> Result { + let group_id = self + .add_logical_expr_to_group_inner(logical_expr, None) + .await?; + Ok(group_id) + } + + async fn get_all_scalar_exprs_in_group( + &self, + group_id: ScalarGroupId, + ) -> Result)>> { + #[derive(sqlx::FromRow)] + struct ScalarExprRecord { + scalar_expression_id: ScalarExpressionId, + data: sqlx::types::Json>, + } + + let mut txn = self.begin().await?; + let representative_group_id = self + .get_representative_scalar_group_id(&mut txn, group_id) + .await?; + let scalar_exprs: Vec = + sqlx::query_as(&self.get_all_scalar_exprs_in_group_query) + .bind(representative_group_id) + .fetch_all(&mut *txn) + .await?; + + txn.commit().await?; + Ok(scalar_exprs + .into_iter() + .map(|record| (record.scalar_expression_id, record.data.0)) + .collect()) + } + + async fn add_scalar_expr_to_group( + &self, + scalar_expr: &ScalarExpression, + group_id: ScalarGroupId, + ) -> Result { + let group_id = self + .add_scalar_expr_to_group_inner(scalar_expr, Some(group_id)) + .await?; + Ok(group_id) + } + + async fn add_scalar_expr(&self, scalar_expr: &ScalarExpression) -> Result { + let group_id = self + .add_scalar_expr_to_group_inner(scalar_expr, None) + .await?; + Ok(group_id) + } + + async fn merge_relation_group( + &self, + from: RelationalGroupId, + to: RelationalGroupId, + ) -> Result { + let mut txn = self.begin().await?; + self.set_representative_group_id(&mut txn, from, to).await?; + txn.commit().await?; + Ok(to) + } + + async fn merge_scalar_group( + &self, + from: ScalarGroupId, + to: ScalarGroupId, + ) -> Result { + let mut txn = self.begin().await?; + self.set_representative_scalar_group_id(&mut txn, from, to) + .await?; + txn.commit().await?; + Ok(to) + } +} + +// Helper functions for implementing the `Memoize` trait. +impl SqliteMemo { + /// Gets the representative group id of a relational group. + async fn get_representative_group_id( + &self, + db: &mut SqliteConnection, + group_id: RelationalGroupId, + ) -> anyhow::Result { + let representative_group_id: RelationalGroupId = + sqlx::query_scalar("SELECT representative_group_id FROM relation_groups WHERE id = $1") + .bind(group_id) + .fetch_one(db) + .await?; + Ok(representative_group_id) + } + + /// Sets the representative group id of a relational group. + async fn set_representative_group_id( + &self, + db: &mut SqliteConnection, + group_id: RelationalGroupId, + representative_group_id: RelationalGroupId, + ) -> anyhow::Result<()> { + sqlx::query("UPDATE relation_groups SET representative_group_id = $1 WHERE representative_group_id = $2") + .bind(representative_group_id) + .bind(group_id) + .execute(db) + .await?; + Ok(()) + } + + /// Gets the representative group id of a scalar group. + async fn get_representative_scalar_group_id( + &self, + db: &mut SqliteConnection, + group_id: ScalarGroupId, + ) -> anyhow::Result { + let representative_group_id: ScalarGroupId = + sqlx::query_scalar("SELECT representative_group_id FROM scalar_groups WHERE id = $1") + .bind(group_id) + .fetch_one(db) + .await?; + Ok(representative_group_id) + } + + /// Sets the representative group id of a scalar group. + async fn set_representative_scalar_group_id( + &self, + db: &mut SqliteConnection, + group_id: ScalarGroupId, + representative_group_id: ScalarGroupId, + ) -> anyhow::Result<()> { + sqlx::query("UPDATE scalar_groups SET representative_group_id = $1 WHERE representative_group_id = $2") + .bind(representative_group_id) + .bind(group_id) + .execute(db) + .await?; + Ok(()) + } + + /// Inserts a scalar expression into the database. If the `add_to_group_id` is `Some`, + /// we will attempt to add the scalar expression to the specified group. + /// If the scalar expression already exists in the database, the existing group id will be returned. + /// Otherwise, a new group id will be created. + async fn add_scalar_expr_to_group_inner( + &self, + scalar_expr: &ScalarExpression, + add_to_group_id: Option, + ) -> anyhow::Result { + let mut txn = self.begin().await?; + let group_id = if let Some(group_id) = add_to_group_id { + self.get_representative_scalar_group_id(&mut txn, group_id) + .await? + } else { + let group_id = txn.new_scalar_group_id().await?; + sqlx::query( + "INSERT INTO scalar_groups (id, representative_group_id, exploration_status) VALUES ($1, $2, $3)", + ) + .bind(group_id) + .bind(group_id) + .bind(ExplorationStatus::Unexplored) + .execute(&mut *txn) + .await?; + group_id + }; + + let scalar_expr_id = txn.new_scalar_expression_id().await?; + let inserted_group_id: ScalarGroupId = match scalar_expr { + ScalarExpression::Constant(constant) => { + Self::insert_into_scalar_expressions( + &mut txn, + scalar_expr_id, + group_id, + ScalarOperatorKind::Constant, + ) + .await?; + + sqlx::query_scalar("INSERT INTO scalar_constants (scalar_expression_id, group_id, value) VALUES ($1, $2, $3) ON CONFLICT DO UPDATE SET group_id = group_id RETURNING group_id") + .bind(scalar_expr_id) + .bind(group_id) + .bind(serde_json::to_string(&constant)?) + .fetch_one(&mut *txn) + .await? + } + ScalarExpression::ColumnRef(column_ref) => { + Self::insert_into_scalar_expressions( + &mut txn, + scalar_expr_id, + group_id, + ScalarOperatorKind::ColumnRef, + ) + .await?; + + sqlx::query_scalar("INSERT INTO scalar_column_refs (scalar_expression_id, group_id, column_index) VALUES ($1, $2, $3) ON CONFLICT DO UPDATE SET group_id = group_id RETURNING group_id") + .bind(scalar_expr_id) + .bind(group_id) + .bind(serde_json::to_string(&column_ref.column_index)?) + .fetch_one(&mut *txn) + .await? + } + ScalarExpression::Add(add) => { + Self::insert_into_scalar_expressions( + &mut txn, + scalar_expr_id, + group_id, + ScalarOperatorKind::Add, + ) + .await?; + + sqlx::query_scalar("INSERT INTO scalar_adds (scalar_expression_id, group_id, left_group_id, right_group_id) VALUES ($1, $2, $3, $4) ON CONFLICT DO UPDATE SET group_id = group_id RETURNING group_id") + .bind(scalar_expr_id) + .bind(group_id) + .bind(add.left) + .bind(add.right) + .fetch_one(&mut *txn) + .await? + } + ScalarExpression::Equal(equal) => { + Self::insert_into_scalar_expressions( + &mut txn, + scalar_expr_id, + group_id, + ScalarOperatorKind::Equal, + ) + .await?; + + sqlx::query_scalar("INSERT INTO scalar_equals (scalar_expression_id, group_id, left_group_id, right_group_id) VALUES ($1, $2, $3, $4) ON CONFLICT DO UPDATE SET group_id = group_id RETURNING group_id") + .bind(scalar_expr_id) + .bind(group_id) + .bind(equal.left) + .bind(equal.right) + .fetch_one(&mut *txn) + .await? + } + }; + + if inserted_group_id == group_id { + // There is no duplicate, we should commit the transaction. + txn.commit().await?; + } else if add_to_group_id.is_some() { + // merge the two groups. + self.set_representative_scalar_group_id(&mut txn, group_id, inserted_group_id) + .await?; + + // We should remove the dangling logical expression. We waste one id here but it is ok. + self.remove_dangling_scalar_expr(&mut txn, scalar_expr_id) + .await?; + txn.commit().await?; + } + Ok(inserted_group_id) + } + + /// Inserts an entry into the `scalar_expressions` table. + async fn insert_into_scalar_expressions( + db: &mut SqliteConnection, + scalar_expr_id: ScalarExpressionId, + group_id: ScalarGroupId, + operator_kind: ScalarOperatorKind, + ) -> anyhow::Result<()> { + sqlx::query("INSERT INTO scalar_expressions (id, group_id, operator_kind, exploration_status) VALUES ($1, $2, $3, $4)") + .bind(scalar_expr_id) + .bind(group_id) + .bind(operator_kind) + .bind(ExplorationStatus::Unexplored) + .execute(&mut *db) + .await?; + Ok(()) + } + + /// Removes a dangling scalar expression from the `scalar_expressions` table. + async fn remove_dangling_scalar_expr( + &self, + db: &mut SqliteConnection, + scalar_expr_id: ScalarExpressionId, + ) -> anyhow::Result<()> { + sqlx::query("DELETE FROM scalar_expressions WHERE id = $1") + .bind(scalar_expr_id) + .execute(db) + .await?; + Ok(()) + } + + /// Inserts a logical expression into the memo table. If the `add_to_group_id` is `Some`, + /// we will attempt to add the logical expression to the specified group. + /// If the logical expression already exists in the database, the existing group id will be returned. + /// Otherwise, a new group id will be created. + async fn add_logical_expr_to_group_inner( + &self, + logical_expr: &LogicalExpression, + add_to_group_id: Option, + ) -> anyhow::Result { + let mut txn = self.begin().await?; + let group_id = if let Some(group_id) = add_to_group_id { + self.get_representative_group_id(&mut txn, group_id).await? + } else { + let group_id = txn.new_relational_group_id().await?; + sqlx::query( + "INSERT INTO relation_groups (id, representative_group_id, exploration_status) VALUES ($1, $2, $3)", + ) + .bind(group_id) + .bind(group_id) + .bind(ExplorationStatus::Unexplored) + .execute(&mut *txn) + .await?; + group_id + }; + + let logical_expr_id = txn.new_logical_expression_id().await?; + + // The inserted group id could be different from the original group id + // if the logical expression already exists in the group. + let inserted_group_id: RelationalGroupId = match logical_expr { + LogicalExpression::Scan(scan) => { + Self::insert_into_logical_expressions( + &mut txn, + logical_expr_id, + group_id, + LogicalOperatorKind::Scan, + ) + .await?; + + sqlx::query_scalar("INSERT INTO scans (logical_expression_id, group_id, table_name, predicate_group_id) VALUES ($1, $2, $3, $4) ON CONFLICT DO UPDATE SET group_id = group_id RETURNING group_id") + .bind(logical_expr_id) + .bind(group_id) + .bind(serde_json::to_string(&scan.table_name)?) + .bind(scan.predicate) + .fetch_one(&mut *txn) + .await? + } + LogicalExpression::Filter(filter) => { + Self::insert_into_logical_expressions( + &mut txn, + logical_expr_id, + group_id, + LogicalOperatorKind::Filter, + ) + .await?; + + sqlx::query_scalar("INSERT INTO filters (logical_expression_id, group_id, child_group_id, predicate_group_id) VALUES ($1, $2, $3, $4) ON CONFLICT DO UPDATE SET group_id = group_id RETURNING group_id") + .bind(logical_expr_id) + .bind(group_id) + .bind(filter.child) + .bind(filter.predicate) + .fetch_one(&mut *txn) + .await? + } + LogicalExpression::Join(join) => { + Self::insert_into_logical_expressions( + &mut txn, + logical_expr_id, + group_id, + LogicalOperatorKind::Join, + ) + .await?; + + sqlx::query_scalar("INSERT INTO joins (logical_expression_id, group_id, join_type, left_group_id, right_group_id, condition_group_id) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT DO UPDATE SET group_id = group_id RETURNING group_id") + .bind(logical_expr_id) + .bind(group_id) + .bind(serde_json::to_string(&join.join_type)?) + .bind(join.left) + .bind(join.right) + .bind(join.condition) + .fetch_one(&mut *txn) + .await? + } + }; + + if inserted_group_id == group_id { + // There is no duplicate, we should commit the transaction. + txn.commit().await?; + } else if add_to_group_id.is_some() { + // Merge the two groups. + self.set_representative_group_id(&mut txn, group_id, inserted_group_id) + .await?; + + // We should remove the dangling logical expression. We waste one id here but it is ok. + self.remove_dangling_logical_expr(&mut txn, logical_expr_id) + .await?; + txn.commit().await?; + } + Ok(inserted_group_id) + } + + /// Inserts an entry into the `logical_expressions` table. + async fn insert_into_logical_expressions( + txn: &mut SqliteConnection, + logical_expr_id: LogicalExpressionId, + group_id: RelationalGroupId, + operator_kind: LogicalOperatorKind, + ) -> anyhow::Result<()> { + sqlx::query("INSERT INTO logical_expressions (id, group_id, operator_kind, exploration_status) VALUES ($1, $2, $3, $4)") + .bind(logical_expr_id) + .bind(group_id) + .bind(operator_kind) + .bind(ExplorationStatus::Unexplored) + .execute(&mut *txn) + .await?; + Ok(()) + } + + /// Removes a dangling logical expression from the `logical_expressions` table. + async fn remove_dangling_logical_expr( + &self, + db: &mut SqliteConnection, + logical_expr_id: LogicalExpressionId, + ) -> anyhow::Result<()> { + sqlx::query("DELETE FROM logical_expressions WHERE id = $1") + .bind(logical_expr_id) + .execute(db) + .await?; + Ok(()) + } +} + +/// The SQL query to get all logical expressions in a group. +/// For each of the operators, the logical_expression_id is selected, +/// as well as the data fields in json form. +const fn get_all_logical_exprs_in_group_query() -> &'static str { + concat!( + "SELECT logical_expression_id, json_object('Scan', json_object('table_name', json(table_name), 'predicate', predicate_group_id)) as data FROM scans WHERE group_id = $1", + " UNION ALL ", + "SELECT logical_expression_id, json_object('Filter', json_object('child', child_group_id, 'predicate', predicate_group_id)) as data FROM filters WHERE group_id = $1", + " UNION ALL ", + "SELECT logical_expression_id, json_object('Join', json_object('join_type', json(join_type), 'left', left_group_id, 'right', right_group_id, 'condition', condition_group_id)) as data FROM joins WHERE group_id = $1" + ) +} + +/// The SQL query to get all scalar expressions in a group. +/// For each of the operators, the scalar_expression_id is selected, +/// as well as the data fields in json form. +const fn get_all_scalar_exprs_in_group_query() -> &'static str { + concat!( + "SELECT scalar_expression_id, json_object('Constant', json(value)) as data FROM scalar_constants WHERE group_id = $1", + " UNION ALL ", + "SELECT scalar_expression_id, json_object('ColumnRef', json_object('column_index', json(column_index))) as data FROM scalar_column_refs WHERE group_id = $1", + " UNION ALL ", + "SELECT scalar_expression_id, json_object('Add', json_object('left', left_group_id, 'right', right_group_id)) as data FROM scalar_adds WHERE group_id = $1", + " UNION ALL ", + "SELECT scalar_expression_id, json_object('Equal', json_object('left', left_group_id, 'right', right_group_id)) as data FROM scalar_equals WHERE group_id = $1" + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::operators::relational::logical::*; + use crate::operators::scalar::*; + use crate::values::OptdValue; + + #[tokio::test] + async fn test_insert_expr_with_memo() -> anyhow::Result<()> { + let memo = SqliteMemo::new_in_memory().await?; + + let true_predicate = + ScalarExpression::Constant(constants::Constant::new(OptdValue::Bool(true))); + let true_predicate_group = memo.add_scalar_expr(&true_predicate).await?; + let scan1 = Arc::new(LogicalExpression::Scan(scan::Scan::new( + "t1", + true_predicate_group, + ))); + let scan1_group = memo.add_logical_expr(&scan1).await?; + let dup_scan1_group = memo.add_logical_expr(&scan1).await?; + assert_eq!(scan1_group, dup_scan1_group); + + let scan2 = Arc::new(LogicalExpression::Scan(scan::Scan::new( + "t2", + true_predicate_group, + ))); + let scan2_group = memo.add_logical_expr(&scan2).await?; + let dup_scan2_group = memo.add_logical_expr(&scan2).await?; + assert_eq!(scan2_group, dup_scan2_group); + + let t1v1 = ScalarExpression::ColumnRef(column_ref::ColumnRef::new(1)); + let t1v1_group_id = memo.add_scalar_expr(&t1v1).await?; + let t2v2 = ScalarExpression::ColumnRef(column_ref::ColumnRef::new(2)); + let t2v2_group_id = memo.add_scalar_expr(&t2v2).await?; + + let join_cond = ScalarExpression::Equal(equal::Equal::new(t1v1_group_id, t2v2_group_id)); + let join_cond_group_id = memo.add_scalar_expr(&join_cond).await?; + let join = Arc::new(LogicalExpression::Join(join::Join::new( + "inner", + scan1_group, + scan2_group, + join_cond_group_id, + ))); + + let join_group = memo.add_logical_expr(&join).await?; + let dup_join_group = memo.add_logical_expr(&join).await?; + assert_eq!(join_group, dup_join_group); + + let join_alt = Arc::new(LogicalExpression::Join(join::Join::new( + "inner", + scan2_group, + scan1_group, + join_cond_group_id, + ))); + let join_alt_group = memo + .add_logical_expr_to_group(&join_alt, join_group) + .await?; + assert_eq!(join_group, join_alt_group); + + let logical_exprs: Vec> = memo + .get_all_logical_exprs_in_group(join_group) + .await? + .into_iter() + .map(|(_, expr)| expr) + .collect(); + assert!(logical_exprs.contains(&join)); + assert!(logical_exprs.contains(&join_alt)); + + let children_groups = join.children_relations(); + assert_eq!(children_groups.len(), 2); + assert_eq!(children_groups[0], scan1_group); + assert_eq!(children_groups[1], scan2_group); + + let children_groups = join_alt.children_relations(); + assert_eq!(children_groups.len(), 2); + assert_eq!(children_groups[0], scan2_group); + assert_eq!(children_groups[1], scan1_group); + + let logical_exprs: Vec> = memo + .get_all_logical_exprs_in_group(scan1_group) + .await? + .into_iter() + .map(|(_, expr)| expr) + .collect(); + assert!(logical_exprs.contains(&scan1)); + assert_eq!(scan1.children_relations().len(), 0); + + let logical_exprs: Vec> = memo + .get_all_logical_exprs_in_group(scan2_group) + .await? + .into_iter() + .map(|(_, expr)| expr) + .collect(); + assert!(logical_exprs.contains(&scan2)); + assert_eq!(scan2.children_relations().len(), 0); + + Ok(()) + } +} diff --git a/optd-core/src/storage/migrations/20250130134420_create_id_sequences.down.sql b/optd-core/src/storage/migrations/20250130134420_create_id_sequences.down.sql new file mode 100644 index 0000000..97f2eaa --- /dev/null +++ b/optd-core/src/storage/migrations/20250130134420_create_id_sequences.down.sql @@ -0,0 +1 @@ +DROP TABLE id_sequences; diff --git a/optd-core/src/storage/migrations/20250130134420_create_id_sequences.up.sql b/optd-core/src/storage/migrations/20250130134420_create_id_sequences.up.sql new file mode 100644 index 0000000..138c16f --- /dev/null +++ b/optd-core/src/storage/migrations/20250130134420_create_id_sequences.up.sql @@ -0,0 +1,11 @@ +-- Table for id sequence generation. This is used to generate unique ids for various entities in the system. +-- In other words, this table only contains one row. +CREATE TABLE id_sequences ( + -- The id of the sequence. + id INTEGER NOT NULL PRIMARY KEY, + -- The current value of the id sequence. + current_value BIGINT NOT NULL +); + +-- Currently, the entire system uses a single sequence for all ids. +INSERT INTO id_sequences (id, current_value) VALUES (0, 0); diff --git a/optd-core/src/storage/migrations/20250130134620_create_relation_groups.down.sql b/optd-core/src/storage/migrations/20250130134620_create_relation_groups.down.sql new file mode 100644 index 0000000..36d3f7c --- /dev/null +++ b/optd-core/src/storage/migrations/20250130134620_create_relation_groups.down.sql @@ -0,0 +1 @@ +DROP TABLE relation_groups; diff --git a/optd-core/src/storage/migrations/20250130134620_create_relation_groups.up.sql b/optd-core/src/storage/migrations/20250130134620_create_relation_groups.up.sql new file mode 100644 index 0000000..2a88950 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130134620_create_relation_groups.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE relation_groups ( + -- A unique identifier for a group of relational expressions in the memo table. + id INTEGER NOT NULL PRIMARY KEY, + -- The representative group id of a relation group. + -- At insertion time, the representative group id is the same as the id, + -- but it can be updated later. + representative_group_id BIGINT NOT NULL, + -- The exploration status of a relation group. + -- It can be one of the following values: Unexplored, Exploring, Explored. + exploration_status INTEGER NOT NULL, + -- The time at which the group is created. + created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + + FOREIGN KEY (representative_group_id) REFERENCES relation_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); diff --git a/optd-core/src/storage/migrations/20250130140546_create_scalar_groups.down.sql b/optd-core/src/storage/migrations/20250130140546_create_scalar_groups.down.sql new file mode 100644 index 0000000..da5650a --- /dev/null +++ b/optd-core/src/storage/migrations/20250130140546_create_scalar_groups.down.sql @@ -0,0 +1 @@ +DROP TABLE scalar_groups; diff --git a/optd-core/src/storage/migrations/20250130140546_create_scalar_groups.up.sql b/optd-core/src/storage/migrations/20250130140546_create_scalar_groups.up.sql new file mode 100644 index 0000000..12bff85 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130140546_create_scalar_groups.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE scalar_groups ( + -- A unique identifier for a group of scalar expressions in the memo table. + id INTEGER NOT NULL PRIMARY KEY, + -- The representative group id of a scalar group. + -- At insertion time, the representative group id is the same as the id, + -- but it can be updated later. + representative_group_id BIGINT NOT NULL, + -- The exploration status of a scalar group. + -- It can be one of the following values: Unexplored, Exploring, Explored. + exploration_status INTEGER NOT NULL, + -- The time at which the group is created. + created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + FOREIGN KEY (representative_group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); diff --git a/optd-core/src/storage/migrations/20250130141152_create_query_instances.down.sql b/optd-core/src/storage/migrations/20250130141152_create_query_instances.down.sql new file mode 100644 index 0000000..ea6b324 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130141152_create_query_instances.down.sql @@ -0,0 +1 @@ +DROP TABLE query_instances; diff --git a/optd-core/src/storage/migrations/20250130141152_create_query_instances.up.sql b/optd-core/src/storage/migrations/20250130141152_create_query_instances.up.sql new file mode 100644 index 0000000..99c06b1 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130141152_create_query_instances.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE query_instances ( + -- A unique identifier for a query instance in the optimizer. + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + -- The SQL query string that is being executed. + query TEXT NOT NULL +); diff --git a/optd-core/src/storage/migrations/20250130144846_create_logical_expressions.down.sql b/optd-core/src/storage/migrations/20250130144846_create_logical_expressions.down.sql new file mode 100644 index 0000000..9ca3cd3 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130144846_create_logical_expressions.down.sql @@ -0,0 +1 @@ +DROP TABLE logical_expressions; diff --git a/optd-core/src/storage/migrations/20250130144846_create_logical_expressions.up.sql b/optd-core/src/storage/migrations/20250130144846_create_logical_expressions.up.sql new file mode 100644 index 0000000..f2ac62f --- /dev/null +++ b/optd-core/src/storage/migrations/20250130144846_create_logical_expressions.up.sql @@ -0,0 +1,22 @@ +CREATE TABLE logical_expressions ( + -- A unique identifier for a logical expression in the optimizer. + id INTEGER NOT NULL PRIMARY KEY, + -- The representative group that a logical expression belongs to. + group_id BIGINT NOT NULL ON CONFLICT REPLACE, + -- The kind of the logical operator. + operator_kind TEXT NOT NULL, + -- The exploration status of a logical expression. + -- It can be one of the following values: Unexplored, Exploring, Explored. + exploration_status INTEGER NOT NULL, + -- The time at which the logical expression is created. + created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + -- When group merging happens, the group id of the logical expression is also updated. + FOREIGN KEY (group_id) REFERENCES relation_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TRIGGER update_logical_expressions_relation_group_ids +AFTER UPDATE OF representative_group_id ON relation_groups +BEGIN + UPDATE OR REPLACE logical_expressions SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; +END; diff --git a/optd-core/src/storage/migrations/20250130144850_create_scalar_expressions.down.sql b/optd-core/src/storage/migrations/20250130144850_create_scalar_expressions.down.sql new file mode 100644 index 0000000..df80356 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130144850_create_scalar_expressions.down.sql @@ -0,0 +1 @@ +DROP TABLE scalar_expressions; diff --git a/optd-core/src/storage/migrations/20250130144850_create_scalar_expressions.up.sql b/optd-core/src/storage/migrations/20250130144850_create_scalar_expressions.up.sql new file mode 100644 index 0000000..203f60f --- /dev/null +++ b/optd-core/src/storage/migrations/20250130144850_create_scalar_expressions.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE scalar_expressions ( + -- A unique identifier for a scalar expression in the optimizer. + id INTEGER NOT NULL PRIMARY KEY, + -- The representative group that a scalar expression belongs to. + group_id BIGINT NOT NULL ON CONFLICT REPLACE, + -- The kind of the scalar operator. + operator_kind TEXT NOT NULL, + -- The exploration status of a scalar expression. + -- It can be one of the following values: Unexplored, Exploring, Explored. + exploration_status INTEGER NOT NULL, + -- The time at which the scalar expression is created. + created_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP) NOT NULL, + -- When group merging happens, the group id of the scalar expression is also updated. + FOREIGN KEY (group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE TRIGGER update_scalar_expressions_scalar_group_ids +AFTER UPDATE OF representative_group_id ON scalar_groups +BEGIN + UPDATE OR REPLACE scalar_expressions SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; +END; + diff --git a/optd-core/src/storage/migrations/20250130151953_create_logical_operator_scans.down.sql b/optd-core/src/storage/migrations/20250130151953_create_logical_operator_scans.down.sql new file mode 100644 index 0000000..16d7356 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130151953_create_logical_operator_scans.down.sql @@ -0,0 +1 @@ +DROP TABLE scans; diff --git a/optd-core/src/storage/migrations/20250130151953_create_logical_operator_scans.up.sql b/optd-core/src/storage/migrations/20250130151953_create_logical_operator_scans.up.sql new file mode 100644 index 0000000..ee11f5e --- /dev/null +++ b/optd-core/src/storage/migrations/20250130151953_create_logical_operator_scans.up.sql @@ -0,0 +1,33 @@ +-- A logical scan operator that reads from a base table. +CREATE TABLE scans ( + -- The logical expression id that this scan is associated with. + logical_expression_id INTEGER NOT NULL PRIMARY KEY, + -- The group id of the scan. + group_id BIGINT NOT NULL, + -- The name of the table. + -- TODO(yuchen): changes this to table id. + table_name JSON NOT NULL, + -- An optional filter expression for predicate pushdown into scan operators. + predicate_group_id BIGINT NOT NULL, + FOREIGN KEY (logical_expression_id) REFERENCES logical_expressions (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES relation_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (predicate_group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Unique index on scan's data fields. +CREATE UNIQUE INDEX scans_data_fields ON scans (table_name, predicate_group_id); + +CREATE TRIGGER update_scans_relation_group_ids +AFTER UPDATE OF representative_group_id ON relation_groups +BEGIN + UPDATE OR REPLACE scans SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; +END; + +CREATE TRIGGER update_scans_scalar_group_ids +AFTER UPDATE OF representative_group_id ON scalar_groups +BEGIN + UPDATE OR REPLACE scans SET predicate_group_id = NEW.representative_group_id WHERE predicate_group_id = OLD.representative_group_id; +END; \ No newline at end of file diff --git a/optd-core/src/storage/migrations/20250130152007_create_logical_operator_filters.down.sql b/optd-core/src/storage/migrations/20250130152007_create_logical_operator_filters.down.sql new file mode 100644 index 0000000..3b98fa3 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130152007_create_logical_operator_filters.down.sql @@ -0,0 +1 @@ +DROP TABLE filters; diff --git a/optd-core/src/storage/migrations/20250130152007_create_logical_operator_filters.up.sql b/optd-core/src/storage/migrations/20250130152007_create_logical_operator_filters.up.sql new file mode 100644 index 0000000..7c0c8bb --- /dev/null +++ b/optd-core/src/storage/migrations/20250130152007_create_logical_operator_filters.up.sql @@ -0,0 +1,37 @@ +-- A logical filter operator that selects rows matching a condition. +CREATE TABLE filters ( + -- The logical expression id that this scan is associated with. + logical_expression_id INTEGER NOT NULL PRIMARY KEY, + -- The group id of the filter. + group_id BIGINT NOT NULL, + -- The input relation. + child_group_id BIGINT NOT NULL, + -- The predicate applied to the child relation: e.g. `column_a > 5`. + predicate_group_id BIGINT NOT NULL, + + FOREIGN KEY (logical_expression_id) REFERENCES logical_expressions (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES relation_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (child_group_id) REFERENCES relation_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (predicate_group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Unique index on filter's data fields. +CREATE UNIQUE INDEX filters_data_fields ON filters (child_group_id, predicate_group_id); + +CREATE TRIGGER update_filters_relation_group_ids +AFTER UPDATE OF representative_group_id ON relation_groups +BEGIN + UPDATE OR REPLACE filters SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; + UPDATE OR REPLACE filters SET child_group_id = NEW.representative_group_id WHERE child_group_id = OLD.representative_group_id; +END; + + +CREATE TRIGGER update_filters_scalar_group_ids +AFTER UPDATE OF representative_group_id ON scalar_groups +BEGIN + UPDATE OR REPLACE filters SET predicate_group_id = NEW.representative_group_id WHERE predicate_group_id = OLD.representative_group_id; +END; diff --git a/optd-core/src/storage/migrations/20250130152019_create_logical_operator_joins.down.sql b/optd-core/src/storage/migrations/20250130152019_create_logical_operator_joins.down.sql new file mode 100644 index 0000000..689037c --- /dev/null +++ b/optd-core/src/storage/migrations/20250130152019_create_logical_operator_joins.down.sql @@ -0,0 +1 @@ +DROP TABLE joins; diff --git a/optd-core/src/storage/migrations/20250130152019_create_logical_operator_joins.up.sql b/optd-core/src/storage/migrations/20250130152019_create_logical_operator_joins.up.sql new file mode 100644 index 0000000..cf111e7 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130152019_create_logical_operator_joins.up.sql @@ -0,0 +1,43 @@ +-- A logical join operator combines rows from two relations. +CREATE TABLE joins ( + -- The logical expression id that this scan is associated with. + logical_expression_id INTEGER NOT NULL PRIMARY KEY, + -- The group id of the join. + group_id BIGINT NOT NULL, + -- The type of the join. + join_type JSON NOT NULL, + -- The left input relation. + left_group_id BIGINT NOT NULL, + -- The right input relation. + right_group_id BIGINT NOT NULL, + -- The join condition. e.g. `left_column_a = right_column_b`. + condition_group_id BIGINT NOT NULL, + + FOREIGN KEY (logical_expression_id) REFERENCES logical_expressions (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES relation_groups (id), + FOREIGN KEY (left_group_id) REFERENCES relation_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE + FOREIGN KEY (right_group_id) REFERENCES relation_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE + FOREIGN KEY (condition_group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Unique index on join's data fields. +CREATE UNIQUE INDEX joins_data_fields ON joins (join_type, left_group_id, right_group_id, condition_group_id); + +CREATE TRIGGER update_joins_relation_group_ids +AFTER UPDATE OF representative_group_id ON relation_groups +BEGIN + UPDATE OR REPLACE joins SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; + UPDATE OR REPLACE joins SET left_group_id = NEW.representative_group_id WHERE left_group_id = OLD.representative_group_id; + UPDATE OR REPLACE joins SET right_group_id = NEW.representative_group_id WHERE right_group_id = OLD.representative_group_id; +END; + + +CREATE TRIGGER update_joins_scalar_group_ids +AFTER UPDATE OF representative_group_id ON scalar_groups +BEGIN + UPDATE OR REPLACE joins SET condition_group_id = NEW.representative_group_id WHERE condition_group_id = OLD.representative_group_id; +END; diff --git a/optd-core/src/storage/migrations/20250130152028_create_logical_operator_projects.down.sql b/optd-core/src/storage/migrations/20250130152028_create_logical_operator_projects.down.sql new file mode 100644 index 0000000..48a1f84 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130152028_create_logical_operator_projects.down.sql @@ -0,0 +1 @@ +DROP TABLE projects; diff --git a/optd-core/src/storage/migrations/20250130152028_create_logical_operator_projects.up.sql b/optd-core/src/storage/migrations/20250130152028_create_logical_operator_projects.up.sql new file mode 100644 index 0000000..d0fce83 --- /dev/null +++ b/optd-core/src/storage/migrations/20250130152028_create_logical_operator_projects.up.sql @@ -0,0 +1,71 @@ +-- A logical project operator takes in a relation and outputs a relation with tuples that +-- contain only specified attributes. +CREATE TABLE projects ( + -- The logical expression id that this project is associated with. + logical_expression_id INTEGER NOT NULL PRIMARY KEY, + -- The group id of the project. + group_id BIGINT NOT NULL, + -- The input relation. + child_group_id BIGINT NOT NULL, + -- The projection list. A vector of scalar group ids, + fields_group_ids JSON NOT NULL, + + FOREIGN KEY (logical_expression_id) REFERENCES logical_expressions (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (child_group_id) REFERENCES relation_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE + -- (Not enforced) + -- FOREIGN KEY json_each(fields_group_ids) REFERENCES scalar_groups (id) + -- ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Unique index on project's data fields. +CREATE UNIQUE INDEX projects_data_fields ON projects (child_group_id, fields_group_ids); + +CREATE TRIGGER update_projects_relation_group_ids +AFTER UPDATE OF representative_group_id ON relation_groups +BEGIN + UPDATE OR REPLACE projects SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; + UPDATE OR REPLACE projects SET child_group_id = NEW.representative_group_id WHERE child_group_id = OLD.representative_group_id; +END; + +-- Approach 1: +CREATE TRIGGER update_projects_scalar_group_ids +AFTER UPDATE OF representative_group_id ON scalar_groups +BEGIN + UPDATE projects SET fields_group_ids = ( + SELECT json_group_array( + CASE + WHEN value = OLD.representative_group_id THEN NEW.representative_group_id + ELSE value + END + ) FROM json_each(fields_group_ids) + ); +END; + +-- Approach 2: +-- CREATE VIEW project_fields AS +-- SELECT +-- logical_expressions.id as logical_expression_id, +-- json_each.value as field_group_id +-- FROM projects, json_each(fields_group_ids); + + +-- CREATE TRIGGER update_projects_scalar_group_ids +-- AFTER UPDATE OF representative_group_id ON scalar_groups +-- BEGIN +-- UPDATE project_fields SET field_group_id = NEW.representative_group_id WHERE field_group_id = OLD.representative_group_id; +-- END; + +-- CREATE TRIGGER update_project_fields +-- INSTEAD OF UPDATE ON project_fields +-- BEGIN +-- UPDATE projects SET fields_group_ids = ( +-- SELECT json_group_array( +-- CASE +-- WHEN value = OLD.field_group_id THEN NEW.field_group_id +-- ELSE value +-- END +-- ) FROM json_each(fields_group_ids) +-- ) WHERE logical_expression_id = OLD.logical_expression_id; +-- END; diff --git a/optd-core/src/storage/migrations/20250203165638_create_scalar_operator_constants.down.sql b/optd-core/src/storage/migrations/20250203165638_create_scalar_operator_constants.down.sql new file mode 100644 index 0000000..db01388 --- /dev/null +++ b/optd-core/src/storage/migrations/20250203165638_create_scalar_operator_constants.down.sql @@ -0,0 +1 @@ +DROP TABLE constants; diff --git a/optd-core/src/storage/migrations/20250203165638_create_scalar_operator_constants.up.sql b/optd-core/src/storage/migrations/20250203165638_create_scalar_operator_constants.up.sql new file mode 100644 index 0000000..53401af --- /dev/null +++ b/optd-core/src/storage/migrations/20250203165638_create_scalar_operator_constants.up.sql @@ -0,0 +1,23 @@ +-- A scalar operator that represents a constant. +CREATE TABLE scalar_constants ( + -- The scalar expression id that this constant is associated with. + scalar_expression_id INTEGER NOT NULL PRIMARY KEY, + -- The group id of the constant. + group_id BIGINT NOT NULL, + -- The value of the constant. + value JSON NOT NULL, + + FOREIGN KEY (scalar_expression_id) REFERENCES scalar_expressions (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Unique index on constant's data fields. +CREATE UNIQUE INDEX scalar_constants_data_fields ON scalar_constants (value); + +CREATE TRIGGER update_scalar_constants_scalar_group_ids +AFTER UPDATE OF representative_group_id ON scalar_groups +BEGIN + UPDATE OR REPLACE scalar_constants SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; +END; diff --git a/optd-core/src/storage/migrations/20250203170054_create_scalar_operator_column_refs.down.sql b/optd-core/src/storage/migrations/20250203170054_create_scalar_operator_column_refs.down.sql new file mode 100644 index 0000000..57279b7 --- /dev/null +++ b/optd-core/src/storage/migrations/20250203170054_create_scalar_operator_column_refs.down.sql @@ -0,0 +1 @@ +DROP TABLE column_refs; \ No newline at end of file diff --git a/optd-core/src/storage/migrations/20250203170054_create_scalar_operator_column_refs.up.sql b/optd-core/src/storage/migrations/20250203170054_create_scalar_operator_column_refs.up.sql new file mode 100644 index 0000000..90f4456 --- /dev/null +++ b/optd-core/src/storage/migrations/20250203170054_create_scalar_operator_column_refs.up.sql @@ -0,0 +1,23 @@ +-- A scalar operator that represents a reference to a column. +CREATE TABLE scalar_column_refs ( + -- The scalar expression id that this column reference is associated with. + scalar_expression_id INTEGER NOT NULL PRIMARY KEY, + -- The group id of the expression. + group_id BIGINT NOT NULL, + -- The value of the column reference. + column_index JSON NOT NULL, + + FOREIGN KEY (scalar_expression_id) REFERENCES scalar_expressions (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Unique index on constant's data fields. +CREATE UNIQUE INDEX scalar_column_refs_data_fields ON scalar_column_refs (column_index); + +CREATE TRIGGER update_scalar_column_refs_scalar_group_ids +AFTER UPDATE OF representative_group_id ON scalar_groups +BEGIN + UPDATE OR REPLACE scalar_column_refs SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; +END; diff --git a/optd-core/src/storage/migrations/20250203170430_create_scalar_operator_adds.down.sql b/optd-core/src/storage/migrations/20250203170430_create_scalar_operator_adds.down.sql new file mode 100644 index 0000000..573e50e --- /dev/null +++ b/optd-core/src/storage/migrations/20250203170430_create_scalar_operator_adds.down.sql @@ -0,0 +1 @@ +DROP TABLE scalar_adds; diff --git a/optd-core/src/storage/migrations/20250203170430_create_scalar_operator_adds.up.sql b/optd-core/src/storage/migrations/20250203170430_create_scalar_operator_adds.up.sql new file mode 100644 index 0000000..213327d --- /dev/null +++ b/optd-core/src/storage/migrations/20250203170430_create_scalar_operator_adds.up.sql @@ -0,0 +1,29 @@ +-- A scalar operator that adds two scalar expressions and returns their sum. +CREATE TABLE scalar_adds ( + -- The scalar expression id that this operator associated with. + scalar_expression_id INTEGER NOT NULL PRIMARY KEY, + -- The group id of the expression. + group_id BIGINT NOT NULL, + -- The group id of left operand of the addition. + left_group_id BIGINT NOT NULL, + -- The group id of right operand of the addition. + right_group_id BIGINT NOT NULL, + + FOREIGN KEY (scalar_expression_id) REFERENCES scalar_expressions (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (left_group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE + FOREIGN KEY (right_group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Unique index on add's data fields. +CREATE UNIQUE INDEX scalar_adds_data_fields ON scalar_adds (left_group_id, right_group_id); + +CREATE TRIGGER update_scalar_adds_scalar_group_ids +AFTER UPDATE OF representative_group_id ON scalar_groups +BEGIN + UPDATE OR REPLACE scalar_adds SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; +END; diff --git a/optd-core/src/storage/migrations/20250203170454_create_scalar_operator_equals.down.sql b/optd-core/src/storage/migrations/20250203170454_create_scalar_operator_equals.down.sql new file mode 100644 index 0000000..cfec6fc --- /dev/null +++ b/optd-core/src/storage/migrations/20250203170454_create_scalar_operator_equals.down.sql @@ -0,0 +1 @@ +DROP TABLE scalar_equals; diff --git a/optd-core/src/storage/migrations/20250203170454_create_scalar_operator_equals.up.sql b/optd-core/src/storage/migrations/20250203170454_create_scalar_operator_equals.up.sql new file mode 100644 index 0000000..1148a3d --- /dev/null +++ b/optd-core/src/storage/migrations/20250203170454_create_scalar_operator_equals.up.sql @@ -0,0 +1,29 @@ +-- A scalar operator that checks if two scalar expressions of the same type are equal. +CREATE TABLE scalar_equals ( + -- The scalar expression id that this operator is associated with. + scalar_expression_id INTEGER NOT NULL PRIMARY KEY, + -- The group id of the expression. + group_id BIGINT NOT NULL, + -- The group id of left operand of the equality. + left_group_id BIGINT NOT NULL, + -- The group id of right operand of the equality. + right_group_id BIGINT NOT NULL, + + FOREIGN KEY (scalar_expression_id) REFERENCES scalar_expressions (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE, + FOREIGN KEY (left_group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE + FOREIGN KEY (right_group_id) REFERENCES scalar_groups (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +-- Unique index on equal's data fields. +CREATE UNIQUE INDEX scalar_equals_data_fields ON scalar_equals (left_group_id, right_group_id); + +CREATE TRIGGER update_scalar_equals_scalar_group_ids +AFTER UPDATE OF representative_group_id ON scalar_groups +BEGIN + UPDATE OR REPLACE scalar_equals SET group_id = NEW.representative_group_id WHERE group_id = OLD.representative_group_id; +END; diff --git a/optd-core/src/storage/mod.rs b/optd-core/src/storage/mod.rs new file mode 100644 index 0000000..1cf9f16 --- /dev/null +++ b/optd-core/src/storage/mod.rs @@ -0,0 +1,2 @@ +pub mod memo; +pub mod transaction; diff --git a/optd-core/src/storage/transaction.rs b/optd-core/src/storage/transaction.rs new file mode 100644 index 0000000..15c8420 --- /dev/null +++ b/optd-core/src/storage/transaction.rs @@ -0,0 +1,158 @@ +//! A transaction that wraps a SQLite transaction while making it easy to generate new identifiers. + +use sqlx::SqliteConnection; +use std::ops::{Deref, DerefMut}; + +use crate::cascades::{ + expressions::{LogicalExpressionId, PhysicalExpressionId, ScalarExpressionId}, + groups::{RelationalGroupId, ScalarGroupId}, +}; + +/// A transaction that wraps a SQLite transaction. +pub struct Transaction<'c> { + /// An active SQLite transaction. + txn: sqlx::Transaction<'c, sqlx::Sqlite>, + /// The current value of the sequence in the transaction. + /// The value is read from the database on transaction start and + /// persisted back to the database on commit. + current_value: i64, +} + +impl Transaction<'_> { + /// Creates a new transaction. + pub async fn new( + mut txn: sqlx::Transaction<'_, sqlx::Sqlite>, + ) -> anyhow::Result> { + let current_value = Sequence::value(&mut txn).await?; + Ok(Transaction { txn, current_value }) + } + + /// Commit the transaction. + pub async fn commit(mut self) -> anyhow::Result<()> { + Sequence::set_value(&mut self.txn, self.current_value).await?; + self.txn.commit().await?; + Ok(()) + } + + /// Gets a new group id. + pub async fn new_relational_group_id(&mut self) -> anyhow::Result { + let id = self.current_value; + self.current_value += 1; + Ok(RelationalGroupId(id)) + } + + /// Gets a new scalar group id. + pub async fn new_scalar_group_id(&mut self) -> anyhow::Result { + let id = self.current_value; + self.current_value += 1; + Ok(ScalarGroupId(id)) + } + + /// Gets a new logical expression id. + pub async fn new_logical_expression_id(&mut self) -> anyhow::Result { + let id = self.current_value; + self.current_value += 1; + Ok(LogicalExpressionId(id)) + } + + /// Gets a new physical expression id. + pub async fn new_physical_expression_id(&mut self) -> anyhow::Result { + let id = self.current_value; + self.current_value += 1; + Ok(PhysicalExpressionId(id)) + } + + /// Gets a new physical expression id. + pub async fn new_scalar_expression_id(&mut self) -> anyhow::Result { + let id = self.current_value; + self.current_value += 1; + Ok(ScalarExpressionId(id)) + } +} + +impl Deref for Transaction<'_> { + type Target = SqliteConnection; + + fn deref(&self) -> &Self::Target { + &self.txn + } +} + +impl DerefMut for Transaction<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.txn + } +} + +/// Sequence is a unique generator for the entities in the optd storage layer. +struct Sequence; + +impl Sequence { + /// Returns the current value in the sequence. + pub async fn value(db: &mut SqliteConnection) -> anyhow::Result { + let value = sqlx::query_scalar!("SELECT current_value FROM id_sequences WHERE id = 0") + .fetch_one(db) + .await?; + Ok(value) + } + + /// Sets the current value of the sequence to the given value. + pub async fn set_value(db: &mut SqliteConnection, value: i64) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE id_sequences SET current_value = ? WHERE id = 0", + value + ) + .execute(db) + .await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use crate::storage::memo::SqliteMemo; + + use super::*; + + /// Test if the sequence is working correctly with the transaction. + #[tokio::test] + async fn test_sequence() -> anyhow::Result<()> { + let storage = SqliteMemo::new_in_memory().await?; + + // Make sure the sequence is initialized. + { + let mut txn = storage.begin().await?; + Sequence::set_value(&mut txn, 0).await?; + txn.commit().await?; + } + + // Test the sequence, rollback in the end. + { + let mut txn = storage.begin().await?; + let value = Sequence::value(&mut txn).await?; + assert_eq!(value, 0); + + for i in 0..5 { + Sequence::set_value(&mut txn, i).await?; + let value = Sequence::value(&mut txn).await?; + assert_eq!(value, i); + } + } + + // Test the sequence, commit in the end. + { + let mut txn = storage.begin().await?; + let value = Sequence::value(&mut txn).await?; + assert_eq!(value, 0); + + for i in 0..5 { + Sequence::set_value(&mut txn, i).await?; + let value = Sequence::value(&mut txn).await?; + assert_eq!(value, i); + } + } + + Ok(()) + } +} diff --git a/optd-core/src/test_utils.rs b/optd-core/src/test_utils.rs new file mode 100644 index 0000000..9ef0f8f --- /dev/null +++ b/optd-core/src/test_utils.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; + +use crate::{ + operators::{ + relational::logical::{filter::Filter, join::Join, scan::Scan, LogicalOperator}, + scalar::{ + add::Add, column_ref::ColumnRef, constants::Constant, equal::Equal, ScalarOperator, + }, + }, + plans::{logical::PartialLogicalPlan, scalar::PartialScalarPlan}, + values::OptdValue, +}; + +pub fn int64(value: i64) -> Arc { + Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::Constant(Constant::new(OptdValue::Int64(value))), + }) +} + +pub fn boolean(value: bool) -> Arc { + Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::Constant(Constant::new(OptdValue::Bool(value))), + }) +} + +pub fn string(value: &str) -> Arc { + Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::Constant(Constant::new(OptdValue::String(value.to_string()))), + }) +} + +pub fn column_ref(column_index: i64) -> Arc { + Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::ColumnRef(ColumnRef::new(column_index)), + }) +} + +pub fn add(left: Arc, right: Arc) -> Arc { + Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::Add(Add::new(left, right)), + }) +} + +pub fn equal( + left: Arc, + right: Arc, +) -> Arc { + Arc::new(PartialScalarPlan::PartialMaterialized { + operator: ScalarOperator::Equal(Equal::new(left, right)), + }) +} + +pub fn scan(table_name: &str, predicate: Arc) -> Arc { + Arc::new(PartialLogicalPlan::PartialMaterialized { + operator: LogicalOperator::Scan(Scan::new(table_name, predicate)), + }) +} + +pub fn filter( + child: Arc, + predicate: Arc, +) -> Arc { + Arc::new(PartialLogicalPlan::PartialMaterialized { + operator: LogicalOperator::Filter(Filter::new(child, predicate)), + }) +} + +pub fn join( + join_type: &str, + left: Arc, + right: Arc, + condition: Arc, +) -> Arc { + Arc::new(PartialLogicalPlan::PartialMaterialized { + operator: LogicalOperator::Join(Join::new(join_type, left, right, condition)), + }) +} diff --git a/optd-core/src/values/mod.rs b/optd-core/src/values/mod.rs new file mode 100644 index 0000000..556ea0c --- /dev/null +++ b/optd-core/src/values/mod.rs @@ -0,0 +1,101 @@ +//! Defines the type system and expressions for OPTD-DSL. +//! +//! This module contains the core type definitions that form the basis of OPTD's domain-specific +//! language. It defines both the primitive values that can be manipulated and the expressions +//! that operate on these values. + +use serde::{Deserialize, Serialize}; + +/// All values supported by the OPTD-DSL. +/// +/// These values form the basic building blocks for the type system and can be +/// used in expressions and pattern matching. +#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] +pub enum OptdValue { + /// 64-bit signed integer + Int64(i64), + /// UTF-8 encoded string + String(String), + /// Boolean value + Bool(bool), + // Complex Types: TODO(alexis). Enums, Optionals, Arrays, etc. +} + +/// Expressions that can be evaluated on OptdValues. +/// +/// This enum defines all possible expressions in the OPTD-DSL, including: +/// - Control flow expressions (if-then-else) +/// - Value references and literals +/// - Comparison operations +/// - Arithmetic operations +/// - Boolean operations +/// +/// TODO(alexis): In the future, it would be nice to support user defined +/// functions on top of the basic expressions. This would enable the support +/// of custom defined checks on the optd values. +#[derive(Clone, Debug)] +pub enum OptdExpr { + /// Conditional expression + IfThenElse { + /// The condition to evaluate + cond: Box, + /// Expression to evaluate if condition is true + then: Box, + /// Expression to evaluate if condition is false + otherwise: Box, + }, + /// Reference to a bound value by name + Ref(String), + /// Literal value + Value(OptdValue), + + /// Equality comparison + Eq { + left: Box, + right: Box, + }, + /// Less than comparison + Lt { + left: Box, + right: Box, + }, + /// Greater than comparison + Gt { + left: Box, + right: Box, + }, + + /// Addition operation + Add { + left: Box, + right: Box, + }, + /// Subtraction operation + Sub { + left: Box, + right: Box, + }, + /// Multiplication operation + Mul { + left: Box, + right: Box, + }, + /// Division operation + Div { + left: Box, + right: Box, + }, + + /// Logical AND + And { + left: Box, + right: Box, + }, + /// Logical OR + Or { + left: Box, + right: Box, + }, + /// Logical NOT + Not(Box), +}