├── .gitignore ├── README.md ├── config.go ├── fakegres.go ├── go.mod ├── go.sum ├── pgEngine.go └── pgServer.go /.gitignore: -------------------------------------------------------------------------------- 1 | fakegres-fdb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fakegres 2 | 3 | Distributed PostgreSQL backed by FoundationDB. 4 | 5 | ## Setup 6 | 7 | Setup FoundationDB (https://apple.github.io/foundationdb/) on your machine. 8 | 9 | ```bash 10 | $ go mod tidy 11 | $ go build 12 | $ ./fakegres-fdb -pg-port=6000 -reset=false -columnar=false 13 | $ psql -h localhost -p 6000 14 | 15 | psql> create table customer (age int, name text); 16 | psql> insert into customer values(14, 'garry'), (20, 'ted'); 17 | psql> select name, age from customer; 18 | ``` 19 | 20 | ## Introduction 21 | 22 | This builds on top of [Fakegres + SQLite](https://github.com/divyenduz/fakegres) ([tweet](https://x.com/divyenduz/status/1759917106743693580)). 23 | 24 | Basically, this is 0.00001 version of SQL over KV, the idea is not new at all. CockroadchDB does it in production and have [heavily documented it](https://github.com/cockroachdb/cockroach/blob/master/pkg/util/encoding/encoding.go). and even Foundation DB had an [SQL layer](https://forums.foundationdb.org/t/sql-layer-in-foundationdb/94/3) that is not longer maintained. 25 | 26 | I wanted to learn data modeling with Foundation DB and this was one my [didn't get to it projects at Recurse](https://blog.divyendusingh.com/p/recurse-center-return-statement). 27 | 28 | The code is heavily commented to show my intent, needless to say this is very WIP. 29 | 30 | ## Resources 31 | 32 | - [Data modeling in Foundation DB](https://apple.github.io/foundationdb/data-modeling.html) 33 | - [The architecture of a distributed SQL database, part 1: Converting SQL to a KV store](https://www.cockroachlabs.com/blog/distributed-sql-key-value-store/) 34 | - [CockroachDB: Architecture of a Geo-Distributed SQL Database](https://youtu.be/OJySfiMKXLs?t=1104) 35 | - [CockroachDB's v3 encoding](https://github.com/cockroachdb/cockroach/blob/master/pkg/util/encoding/encoding.go) 36 | - [CockroachDB's v2 encoding](https://www.cockroachlabs.com/blog/sql-cockroachdb-column-families/) 37 | - [CockroachDB's v1 encoding](https://www.cockroachlabs.com/blog/sql-in-cockroachdb-mapping-table-data-to-key-value-storage/) 38 | - [FoundationDB SQL Layer (community)](https://github.com/qiukeren/foundationdb-sql-layer) 39 | - [What's the big deal about key-value databases like FoundationDB and RocksDB?](https://notes.eatonphil.com/whats-the-big-deal-about-key-value-databases.html) 40 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | ) 7 | 8 | type config struct { 9 | columnar bool 10 | reset bool 11 | pgPort string 12 | } 13 | 14 | func getConfig() config { 15 | cfg := config{} 16 | flag.BoolVar(&cfg.columnar, "columnar", false, "Open the database in columnar mode") 17 | flag.BoolVar(&cfg.reset, "reset", false, "Reset the database on startup") 18 | flag.StringVar(&cfg.pgPort, "pg-port", "6000", "Port to listen on for PostgreSQL connections") 19 | flag.Parse() 20 | log.Println("cfg: ", cfg) 21 | return cfg 22 | } 23 | -------------------------------------------------------------------------------- /fakegres.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/apple/foundationdb/bindings/go/src/fdb" 7 | ) 8 | 9 | func main() { 10 | cfg := getConfig() 11 | 12 | fdb.MustAPIVersion(710) 13 | db := fdb.MustOpenDefault() 14 | 15 | if cfg.reset { 16 | db.Transact(func(tr fdb.Transaction) (interface{}, error) { 17 | tr.ClearRange(fdb.KeyRange{Begin: fdb.Key{}, End: fdb.Key{0xFF}}) 18 | log.Println("All keys have been deleted from the database.") 19 | return nil, nil 20 | }) 21 | } 22 | 23 | runPgServer(cfg.pgPort, db, cfg) 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module fakegres-fdb 2 | 3 | go 1.21.6 4 | 5 | require ( 6 | github.com/apple/foundationdb/bindings/go v0.0.0-20240723142048-7aad24e407e6 7 | github.com/google/uuid v1.6.0 8 | github.com/jackc/pgproto3/v2 v2.3.2 9 | github.com/pganalyze/pg_query_go/v2 v2.2.0 10 | ) 11 | 12 | require ( 13 | github.com/golang/protobuf v1.5.2 // indirect 14 | github.com/google/go-cmp v0.5.9 // indirect 15 | github.com/jackc/chunkreader/v2 v2.0.0 // indirect 16 | github.com/jackc/pgio v1.0.0 // indirect 17 | github.com/stretchr/testify v1.8.4 // indirect 18 | google.golang.org/protobuf v1.28.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/apple/foundationdb/bindings/go v0.0.0-20240723142048-7aad24e407e6 h1:lkIvkQ4C7M+oWhBaYBhIJMC4MD4Yif2BPfwdlCKvQMA= 2 | github.com/apple/foundationdb/bindings/go v0.0.0-20240723142048-7aad24e407e6/go.mod h1:OMVSB21p9+xQUIqlGizHPZfjK+SHws1ht+ZytVDoz9U= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 7 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 8 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 9 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 10 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 11 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 12 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 13 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 14 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 15 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 16 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 17 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 18 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 19 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 20 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 21 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 22 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 23 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 24 | github.com/jackc/chunkreader/v2 v2.0.0 h1:DUwgMQuuPnS0rhMXenUtZpqZqrR/30NWY+qQvTpSvEs= 25 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 26 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 27 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 28 | github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= 29 | github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 30 | github.com/pganalyze/pg_query_go/v2 v2.2.0 h1:OW+reH+ZY7jdEuPyuLGlf1m7dLbE+fDudKXhLs0Ttpk= 31 | github.com/pganalyze/pg_query_go/v2 v2.2.0/go.mod h1:XAxmVqz1tEGqizcQ3YSdN90vCOHBWjJi8URL1er5+cA= 32 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 33 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 35 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 36 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 37 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 38 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 39 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 40 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 41 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 42 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 43 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 44 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 45 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 46 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 47 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 48 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 50 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 51 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 52 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /pgEngine.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/apple/foundationdb/bindings/go/src/fdb" 9 | "github.com/apple/foundationdb/bindings/go/src/fdb/directory" 10 | "github.com/apple/foundationdb/bindings/go/src/fdb/tuple" 11 | "github.com/google/uuid" 12 | pgquery "github.com/pganalyze/pg_query_go/v2" 13 | ) 14 | 15 | type pgEngine struct { 16 | db fdb.Transactor 17 | } 18 | 19 | func newPgEngine(db fdb.Transactor) pgEngine { 20 | return pgEngine{db} 21 | } 22 | 23 | func (pe pgEngine) execute(tree pgquery.ParseResult) error { 24 | for _, stmt := range tree.GetStmts() { 25 | n := stmt.GetStmt() 26 | if c := n.GetCreateStmt(); c != nil { 27 | return pe.executeCreate(c) 28 | } 29 | 30 | if c := n.GetInsertStmt(); c != nil { 31 | return pe.executeInsert(c) 32 | } 33 | 34 | if c := n.GetDeleteStmt(); c != nil { 35 | return pe.executeDelete(c) 36 | } 37 | 38 | if c := n.GetSelectStmt(); c != nil { 39 | _, err := pe.executeSelect(c) 40 | return err 41 | } 42 | } 43 | 44 | return nil 45 | } 46 | 47 | type tableDefinition struct { 48 | Name string 49 | ColumnNames []string 50 | ColumnTypes []string 51 | } 52 | 53 | /* 54 | Parse the create table SQL statement and create an equivalent KV structure in the database. 55 | 56 | Example: 57 | 58 | # The following SQL 59 | 60 | ```sql 61 | create table user (age int, name text); 62 | ``` 63 | 64 | # Will produce the following KV structure 65 | 66 | ``` 67 | catalog/table/user: "" (empty value to mark that the table exists) 68 | catalog/table/user/age: int 69 | catalog/table/user/name: text 70 | ``` 71 | 72 | Keys in FoundationDB are globally sorted, so retrieving all the metadata for a table is 73 | usually a single query. 74 | */ 75 | func (pe pgEngine) executeCreate(stmt *pgquery.CreateStmt) error { 76 | tbl := tableDefinition{} 77 | tbl.Name = stmt.Relation.Relname 78 | 79 | catalogDir, err := directory.CreateOrOpen(pe.db, []string{"catalog"}, nil) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | tableSS := catalogDir.Sub("table") 84 | tableKey := tableSS.Pack(tuple.Tuple{tbl.Name}) 85 | 86 | _, err = pe.db.Transact(func(tr fdb.Transaction) (ret interface{}, err error) { 87 | 88 | if tr.Get(tableKey).MustGet() != nil { 89 | log.Printf("Table %s already exists", tbl.Name) 90 | return 91 | } 92 | 93 | // Note: table exists, marked by empty value and table name as key 94 | tr.Set(tableSS.Pack(tuple.Tuple{tbl.Name}), []byte("")) 95 | 96 | for _, c := range stmt.TableElts { 97 | cd := c.GetColumnDef() 98 | 99 | // Names is namespaced. So `INT` is pg_catalog.int4. `BIGINT` is pg_catalog.int8. 100 | var columnType string 101 | for _, n := range cd.TypeName.Names { 102 | if columnType != "" { 103 | columnType += "." 104 | } 105 | columnType += n.GetString_().Str 106 | } 107 | tr.Set(tableSS.Pack(tuple.Tuple{tbl.Name, cd.Colname}), []byte(columnType)) 108 | } 109 | 110 | return 111 | }) 112 | 113 | if err != nil { 114 | return fmt.Errorf("could not create table: %s", err) 115 | } 116 | 117 | return nil 118 | } 119 | 120 | /* 121 | 122 | Get the table definition from the database. This can be done with a single range query. 123 | 124 | */ 125 | 126 | func (pe pgEngine) getTableDefinition(name string) (*tableDefinition, error) { 127 | var tbl tableDefinition 128 | 129 | // TODO: check if table exists, etc. 130 | tbl.Name = name 131 | 132 | catalogDir, err := directory.CreateOrOpen(pe.db, []string{"catalog"}, nil) 133 | if err != nil { 134 | log.Fatal(err) 135 | } 136 | 137 | tableSS := catalogDir.Sub("table") 138 | 139 | _, err = pe.db.ReadTransact(func(rtr fdb.ReadTransaction) (interface{}, error) { 140 | ri := rtr.GetRange(tableSS.Sub(name), fdb.RangeOptions{ 141 | Mode: fdb.StreamingModeWantAll, 142 | }).Iterator() 143 | for ri.Advance() { 144 | kv := ri.MustGet() 145 | t, _ := tableSS.Unpack(kv.Key) 146 | 147 | // Note: deconstruct the key from catalog/table/user/age and extract the column name 148 | tbl.ColumnNames = append(tbl.ColumnNames, t[1].(string)) 149 | tbl.ColumnTypes = append(tbl.ColumnTypes, string(kv.Value)) 150 | } 151 | return nil, nil 152 | }) 153 | if err != nil { 154 | return nil, fmt.Errorf("could not get table defn: %s", err) 155 | } 156 | return &tbl, err 157 | } 158 | 159 | /* 160 | 161 | Parse the insert statement and insert data into the table. 162 | 163 | Example: 164 | 165 | The following SQL 166 | 167 | ```sql 168 | insert into user values(14, 'garry'), (20, 'ted'); 169 | ``` 170 | 171 | Note that since keys are sorted, CREATE TABLE and positional ORDER of INSERT can be different. 172 | 173 | Will produce the following KV structure 174 | 175 | ``` 176 | data/table_data/user/age/72746a7f-727f-4e0a-88f1-d983fea5c158: 14 177 | data/table_data/user/age/34e7ff77-1bed-4ebd-be56-4b966e67c595: 20 178 | data/table_data/user/name/72746a7f-727f-4e0a-88f1-d983fea5c158: garry 179 | data/table_data/user/name/34e7ff77-1bed-4ebd-be56-4b966e67c595: ted 180 | ``` 181 | 182 | Keys in FoundationDB are globally sorted, so the data for this table would be a single query. 183 | However, in this structure (column first) we will receive the table cells in age, age, name, name order and we will need to 184 | collect them in order in select. 185 | 186 | This property of Foundation DB is very interesting. Quoting the docs: https://apple.github.io/foundationdb/data-modeling.html 187 | > You can make your model row-oriented or column-oriented by placing either the row or column first 188 | > in the tuple, respectively. Because the lexicographic order sorts tuple elements from left to right, 189 | > access is optimized for the element placed first. Placing the row first makes it efficient to read all 190 | > the cells in a particular row; reversing the order makes reading a column more efficient. 191 | 192 | We can insert columnar and row based data in the same table in same transaction and still be able to read them efficiently. 193 | Note for future. 194 | 195 | If this was row based, the keys in the database would be: 196 | 197 | data/table_data/user/age/72746a7f-727f-4e0a-88f1-d983fea5c158: 14 198 | data/table_data/user/name/72746a7f-727f-4e0a-88f1-d983fea5c158: garry 199 | data/table_data/user/age/34e7ff77-1bed-4ebd-be56-4b966e67c595: 20 200 | data/table_data/user/name/34e7ff77-1bed-4ebd-be56-4b966e67c595: ted 201 | 202 | And reading them in select would be easier. 203 | */ 204 | 205 | func (pe pgEngine) executeInsert(stmt *pgquery.InsertStmt) error { 206 | tblName := stmt.Relation.Relname 207 | slct := stmt.GetSelectStmt().GetSelectStmt() 208 | 209 | tbl, err := pe.getTableDefinition(tblName) 210 | if err != nil { 211 | return err 212 | } 213 | 214 | catalogDir, err := directory.CreateOrOpen(pe.db, []string{"catalog"}, nil) 215 | if err != nil { 216 | log.Fatal(err) 217 | } 218 | tableSS := catalogDir.Sub("table") 219 | tableKey := tableSS.Pack(tuple.Tuple{tblName}) 220 | 221 | dataDir, err := directory.CreateOrOpen(pe.db, []string{"data"}, nil) 222 | if err != nil { 223 | log.Fatal(err) 224 | } 225 | tableDataSS := dataDir.Sub("table_data") 226 | 227 | _, err = pe.db.Transact(func(tr fdb.Transaction) (ret interface{}, err error) { 228 | if tr.Get(tableKey).MustGet() == nil { 229 | log.Printf("Table %s does not exist", tblName) 230 | return 231 | } 232 | 233 | for _, values := range slct.ValuesLists { 234 | id := uuid.New().String() 235 | columnIndex := 0 236 | maxColumnIndex := len(tbl.ColumnNames) - 1 237 | for _, value := range values.GetList().Items { 238 | if c := value.GetAConst(); c != nil { 239 | if s := c.Val.GetString_(); s != nil { 240 | // Columnar data 241 | tr.Set(tableDataSS.Pack(tuple.Tuple{tblName, "c", tbl.ColumnNames[columnIndex], id}), []byte(s.Str)) 242 | log.Printf("Inserted key c: %s", tableDataSS.Pack(tuple.Tuple{tblName, "c", tbl.ColumnNames[columnIndex], id})) 243 | // Row based data 244 | tr.Set(tableDataSS.Pack(tuple.Tuple{tblName, "r", id, tbl.ColumnNames[columnIndex]}), []byte(s.Str)) 245 | log.Printf("Inserted key r: %s", tableDataSS.Pack(tuple.Tuple{tblName, "r", id, tbl.ColumnNames[columnIndex]})) 246 | 247 | if columnIndex < maxColumnIndex { 248 | columnIndex += 1 249 | } 250 | continue 251 | } 252 | 253 | if i := c.Val.GetInteger(); i != nil { 254 | // TODO: better convert in to byte[], with this conversion, it ends up being a string 255 | valueJson, _ := json.Marshal(i.Ival) 256 | // Columnar data 257 | tr.Set(tableDataSS.Pack(tuple.Tuple{tblName, "c", tbl.ColumnNames[columnIndex], id}), valueJson) 258 | log.Printf("Inserted key c: %s", tableDataSS.Pack(tuple.Tuple{tblName, "c", tbl.ColumnNames[columnIndex], id})) 259 | // Row based data 260 | tr.Set(tableDataSS.Pack(tuple.Tuple{tblName, "r", id, tbl.ColumnNames[columnIndex]}), valueJson) 261 | log.Printf("Inserted key r: %s", tableDataSS.Pack(tuple.Tuple{tblName, "r", id, tbl.ColumnNames[columnIndex]})) 262 | 263 | if columnIndex < maxColumnIndex { 264 | columnIndex += 1 265 | } 266 | continue 267 | } 268 | } 269 | 270 | return nil, fmt.Errorf("unknown value type: %s", value) 271 | } 272 | } 273 | return nil, nil 274 | }) 275 | if err != nil { 276 | return fmt.Errorf("could not insert into the table table: %s", err) 277 | } 278 | 279 | return nil 280 | } 281 | 282 | /* 283 | 284 | Parse the delete statement and delete data from the table. 285 | Currently, this doesn't support where clause and deletes all the data from the table. 286 | 287 | */ 288 | 289 | func (pe pgEngine) executeDelete(stmt *pgquery.DeleteStmt) error { 290 | 291 | catalogDir, err := directory.CreateOrOpen(pe.db, []string{"catalog"}, nil) 292 | if err != nil { 293 | log.Fatal(err) 294 | } 295 | tableSS := catalogDir.Sub("table") 296 | tableKey := tableSS.Pack(tuple.Tuple{stmt.Relation.Relname}) 297 | 298 | dataDir, err := directory.CreateOrOpen(pe.db, []string{"data"}, nil) 299 | if err != nil { 300 | log.Fatal(err) 301 | } 302 | tableDataSS := dataDir.Sub("table_data") 303 | 304 | // TODO: implement where, delete for now deletes everything from the table 305 | 306 | _, err = pe.db.Transact(func(tr fdb.Transaction) (interface{}, error) { 307 | if tr.Get(tableKey).MustGet() == nil { 308 | log.Printf("Table %s does not exist", stmt.Relation.Relname) 309 | return nil, nil 310 | } 311 | 312 | ri := tr.GetRange(tableDataSS, fdb.RangeOptions{ 313 | Mode: fdb.StreamingModeWantAll, 314 | }).Iterator() 315 | for ri.Advance() { 316 | kv := ri.MustGet() 317 | tr.Clear(kv.Key) 318 | } 319 | return nil, nil 320 | }) 321 | if err != nil { 322 | return fmt.Errorf("could not delete table: %s", err) 323 | } 324 | return nil 325 | } 326 | 327 | type pgResult struct { 328 | fieldNames []string 329 | fieldTypes []string 330 | rows [][]any 331 | } 332 | 333 | /* 334 | 335 | Parse the select statement and return the result. 336 | 337 | Example: 338 | 339 | The following SQL: 340 | 341 | ```sql 342 | select name, age from customer; 343 | ``` 344 | 345 | Will produce the following KV structure: 346 | 347 | ``` 348 | data/table_data/user/age/72746a7f-727f-4e0a-88f1-d983fea5c158: 14 349 | data/table_data/user/age/34e7ff77-1bed-4ebd-be56-4b966e67c595: 20 350 | data/table_data/user/name/72746a7f-727f-4e0a-88f1-d983fea5c158: garry 351 | data/table_data/user/name/34e7ff77-1bed-4ebd-be56-4b966e67c595: ted 352 | ``` 353 | 354 | The Select code collects them into [[14, garry], [20, ted]] and returns the result accordingly. 355 | */ 356 | 357 | func (pe pgEngine) executeSelectColumnar(stmt *pgquery.SelectStmt) (*pgResult, error) { 358 | tblName := stmt.FromClause[0].GetRangeVar().Relname 359 | tbl, err := pe.getTableDefinition(tblName) 360 | if err != nil { 361 | return nil, err 362 | } 363 | 364 | results := &pgResult{} 365 | for _, c := range stmt.TargetList { 366 | fieldName := c.GetResTarget().Val.GetColumnRef().Fields[0].GetString_().Str 367 | results.fieldNames = append(results.fieldNames, fieldName) 368 | 369 | fieldType := "" 370 | for i, cn := range tbl.ColumnNames { 371 | if cn == fieldName { 372 | fieldType = tbl.ColumnTypes[i] 373 | } 374 | } 375 | 376 | if fieldType == "" { 377 | return nil, fmt.Errorf("unknown field: %s", fieldName) 378 | } 379 | 380 | results.fieldTypes = append(results.fieldTypes, fieldType) 381 | } 382 | 383 | dataDir, err := directory.CreateOrOpen(pe.db, []string{"data"}, nil) 384 | if err != nil { 385 | log.Fatal(err) 386 | } 387 | tableDataSS := dataDir.Sub("table_data") 388 | 389 | _, _ = pe.db.Transact(func(tr fdb.Transaction) (interface{}, error) { 390 | query := tableDataSS.Pack(tuple.Tuple{tbl.Name, "c"}) 391 | rangeQuery, _ := fdb.PrefixRange(query) 392 | ri := tr.GetRange(rangeQuery, fdb.RangeOptions{ 393 | Mode: fdb.StreamingModeWantAll, 394 | }).Iterator() 395 | 396 | var columnOrder []string 397 | var targetRows [][]any 398 | targetRows = append(targetRows, []any{}) 399 | rowIndex := -1 400 | lastColumn := "" 401 | for ri.Advance() { 402 | kv := ri.MustGet() 403 | t, _ := tableDataSS.Unpack(kv.Key) 404 | 405 | currentTableName := t[0].(string) 406 | currentColumnFormat := t[1].(string) 407 | currentColumnName := t[2].(string) 408 | currentInternalRowId := t[3].(string) 409 | log.Println("fetching row metadata: ", currentTableName, currentColumnFormat, currentColumnName, currentInternalRowId) 410 | if currentColumnName != lastColumn { 411 | rowIndex = 0 412 | lastColumn = currentColumnName 413 | columnOrder = append(columnOrder, currentColumnName) 414 | } else { 415 | targetRows = append(targetRows, []any{}) 416 | } 417 | 418 | for _, target := range results.fieldNames { 419 | if target == currentColumnName { 420 | targetRows[rowIndex] = append(targetRows[rowIndex], string(kv.Value)) 421 | } 422 | } 423 | rowIndex += 1 424 | } 425 | results.fieldNames = columnOrder 426 | 427 | // TODO: don't add empty arrays in the first place 428 | var targetRowsFinal [][]any 429 | targetRows = append(targetRows, []any{}) 430 | for _, row := range targetRows { 431 | if len(row) > 0 { 432 | targetRowsFinal = append(targetRowsFinal, row) 433 | } 434 | } 435 | results.rows = targetRowsFinal 436 | return results, nil 437 | }) 438 | 439 | return results, nil 440 | } 441 | 442 | func (pe pgEngine) executeSelect(stmt *pgquery.SelectStmt) (*pgResult, error) { 443 | tblName := stmt.FromClause[0].GetRangeVar().Relname 444 | tbl, err := pe.getTableDefinition(tblName) 445 | if err != nil { 446 | return nil, err 447 | } 448 | 449 | results := &pgResult{} 450 | for _, c := range stmt.TargetList { 451 | fieldName := c.GetResTarget().Val.GetColumnRef().Fields[0].GetString_().Str 452 | results.fieldNames = append(results.fieldNames, fieldName) 453 | 454 | fieldType := "" 455 | for i, cn := range tbl.ColumnNames { 456 | if cn == fieldName { 457 | fieldType = tbl.ColumnTypes[i] 458 | } 459 | } 460 | 461 | if fieldType == "" { 462 | return nil, fmt.Errorf("unknown field: %s", fieldName) 463 | } 464 | 465 | results.fieldTypes = append(results.fieldTypes, fieldType) 466 | } 467 | 468 | dataDir, err := directory.CreateOrOpen(pe.db, []string{"data"}, nil) 469 | if err != nil { 470 | log.Fatal(err) 471 | } 472 | tableDataSS := dataDir.Sub("table_data") 473 | 474 | _, _ = pe.db.Transact(func(tr fdb.Transaction) (interface{}, error) { 475 | query := tableDataSS.Pack(tuple.Tuple{tbl.Name, "r"}) 476 | rangeQuery, _ := fdb.PrefixRange(query) 477 | ri := tr.GetRange(rangeQuery, fdb.RangeOptions{ 478 | Mode: fdb.StreamingModeWantAll, 479 | }).Iterator() 480 | 481 | var targetRows [][]any 482 | targetRows = append(targetRows, []any{}) 483 | rowIndex := 0 484 | columnOrder := []string{} 485 | for ri.Advance() { 486 | kv := ri.MustGet() 487 | t, _ := tableDataSS.Unpack(kv.Key) 488 | 489 | currentTableName := t[0].(string) 490 | currentColumnFormat := t[1].(string) 491 | currentInternalRowId := t[2].(string) 492 | currentColumnName := t[3].(string) 493 | log.Println("fetching row metadata: ", currentTableName, currentColumnFormat, currentColumnName, currentInternalRowId) 494 | 495 | if len(columnOrder) < len(results.fieldNames) { 496 | columnOrder = append(columnOrder, currentColumnName) 497 | } 498 | if len(targetRows[rowIndex]) == len(results.fieldNames) { 499 | rowIndex += 1 500 | targetRows = append(targetRows, []any{}) 501 | } 502 | 503 | targetRows[rowIndex] = append(targetRows[rowIndex], string(kv.Value)) 504 | } 505 | results.fieldNames = columnOrder 506 | 507 | // TODO: don't add empty arrays in the first place 508 | var targetRowsFinal [][]any 509 | targetRows = append(targetRows, []any{}) 510 | for _, row := range targetRows { 511 | if len(row) > 0 { 512 | targetRowsFinal = append(targetRowsFinal, row) 513 | } 514 | } 515 | results.rows = targetRowsFinal 516 | return results, nil 517 | }) 518 | 519 | return results, nil 520 | } 521 | -------------------------------------------------------------------------------- /pgServer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net" 8 | "strings" 9 | 10 | "github.com/apple/foundationdb/bindings/go/src/fdb" 11 | 12 | "github.com/jackc/pgproto3/v2" 13 | pgquery "github.com/pganalyze/pg_query_go/v2" 14 | ) 15 | 16 | var dataTypeOIDMap = map[string]uint32{ 17 | "text": 25, 18 | "pg_catalog.int4": 23, 19 | } 20 | 21 | type pgServer struct { 22 | conn net.Conn 23 | db fdb.Transactor 24 | cfg config 25 | } 26 | 27 | func (pgs pgServer) done(buf []byte, msg string) { 28 | buf = (&pgproto3.CommandComplete{CommandTag: []byte(msg)}).Encode(buf) 29 | buf = (&pgproto3.ReadyForQuery{TxStatus: 'I'}).Encode(buf) 30 | _, err := pgs.conn.Write(buf) 31 | if err != nil { 32 | log.Printf("failed to write query response: %s", err) 33 | } 34 | } 35 | 36 | func (pgs pgServer) writePgResult(res *pgResult) { 37 | rd := &pgproto3.RowDescription{} 38 | for i, field := range res.fieldNames { 39 | rd.Fields = append(rd.Fields, pgproto3.FieldDescription{ 40 | Name: []byte(field), 41 | DataTypeOID: dataTypeOIDMap[res.fieldTypes[i]], 42 | }) 43 | } 44 | buf := rd.Encode(nil) 45 | for _, row := range res.rows { 46 | dr := &pgproto3.DataRow{} 47 | for _, value := range row { 48 | bs, err := json.Marshal(value) 49 | if err != nil { 50 | log.Printf("Failed to marshal cell: %s\n", err) 51 | return 52 | } 53 | 54 | dr.Values = append(dr.Values, bs) 55 | } 56 | 57 | buf = dr.Encode(buf) 58 | } 59 | 60 | pgs.done(buf, fmt.Sprintf("SELECT %d", len(res.rows))) 61 | } 62 | 63 | func (pgs pgServer) handleStartupMessage(pgconn *pgproto3.Backend) error { 64 | startupMessage, err := pgconn.ReceiveStartupMessage() 65 | if err != nil { 66 | return fmt.Errorf("error receiving startup message: %s", err) 67 | } 68 | 69 | switch startupMessage.(type) { 70 | case *pgproto3.StartupMessage: 71 | buf := (&pgproto3.AuthenticationOk{}).Encode(nil) 72 | buf = (&pgproto3.ReadyForQuery{TxStatus: 'I'}).Encode(buf) 73 | _, err = pgs.conn.Write(buf) 74 | if err != nil { 75 | return fmt.Errorf("error sending ready for query: %s", err) 76 | } 77 | 78 | return nil 79 | case *pgproto3.SSLRequest: 80 | _, err = pgs.conn.Write([]byte("N")) 81 | if err != nil { 82 | return fmt.Errorf("error sending deny SSL request: %s", err) 83 | } 84 | 85 | return pgs.handleStartupMessage(pgconn) 86 | default: 87 | return fmt.Errorf("unknown startup message: %#v", startupMessage) 88 | } 89 | } 90 | 91 | func (pgs pgServer) handleMessage(pgc *pgproto3.Backend) error { 92 | msg, receive_err := pgc.Receive() 93 | if receive_err != nil { 94 | return fmt.Errorf("error receiving message: %s", receive_err) 95 | } 96 | 97 | switch t := msg.(type) { 98 | case *pgproto3.Query: 99 | stmts, parse_err := pgquery.Parse(t.String) 100 | if parse_err != nil { 101 | return fmt.Errorf("error parsing query: %s", receive_err) 102 | } 103 | 104 | if len(stmts.GetStmts()) > 1 { 105 | return fmt.Errorf("only make one request at a time") 106 | } 107 | 108 | stmt := stmts.GetStmts()[0] 109 | 110 | // Handle SELECTs here 111 | s := stmt.GetStmt().GetSelectStmt() 112 | var res *pgResult 113 | var err error 114 | if s != nil { 115 | pe := newPgEngine(pgs.db) 116 | if pgs.cfg.columnar { 117 | res, err = pe.executeSelectColumnar(s) 118 | 119 | } else { 120 | res, err = pe.executeSelect(s) 121 | } 122 | 123 | if err != nil { 124 | return err 125 | } 126 | 127 | pgs.writePgResult(res) 128 | return nil 129 | } else { 130 | pe := newPgEngine(pgs.db) 131 | pe.execute(*stmts) 132 | } 133 | 134 | pgs.done(nil, strings.ToUpper(strings.Split(t.String, " ")[0])+" ok") 135 | case *pgproto3.Terminate: 136 | return nil 137 | default: 138 | return fmt.Errorf("received message other than Query from client: %s", msg) 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (pgs pgServer) handle() { 145 | pgc := pgproto3.NewBackend(pgproto3.NewChunkReader(pgs.conn), pgs.conn) 146 | defer pgs.conn.Close() 147 | 148 | err := pgs.handleStartupMessage(pgc) 149 | if err != nil { 150 | log.Println(err) 151 | return 152 | } 153 | 154 | for { 155 | err := pgs.handleMessage(pgc) 156 | if err != nil { 157 | log.Println(err) 158 | return 159 | } 160 | } 161 | } 162 | 163 | func runPgServer(port string, db fdb.Transactor, cfg config) { 164 | ln, err := net.Listen("tcp", "localhost:"+port) 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | 169 | for { 170 | conn, err := ln.Accept() 171 | if err != nil { 172 | log.Fatal(err) 173 | } 174 | 175 | pc := pgServer{conn, db, cfg} 176 | go pc.handle() 177 | } 178 | } 179 | --------------------------------------------------------------------------------